diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index aa76195519..6f550682f4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -4,7 +4,6 @@ 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.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* 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 15bcdad9af..3907116097 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 @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse -import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase @@ -384,7 +383,7 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fee = fee, providerId = swapProvider.providerId, - userWalletId = requireNotNull(getSelectedWallet()).walletId + userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } ExchangeProviderType.DEX -> { @@ -485,7 +484,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fee: TxFee, providerId: String, - userWalletId: UserWalletId + userWalletId: UserWalletId, ): TxState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), @@ -511,37 +510,38 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( requireNotNull(txData), userWallet = requireNotNull(getSelectedWallet()), - network = currencyToSend.currency.network + network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when(it){ + when (it) { is SendTransactionError.NetworkError -> TxState.NetworkError is SendTransactionError.DataError -> TxState.BlockchainError SendTransactionError.DemoCardError -> TxState.UnknownError else -> TxState.UnknownError } - }, ifRight = { - TxState.TxSent( - fromAmount = amountFormatter.formatSwapAmountToUI( - amount, - currencyToSend.currency.symbol, - ), - toAmount = amountFormatter.formatSwapAmountToUI( - exchangeData.dataModel.toTokenAmount, - currencyToGet.currency.symbol, - ), - txAddress = userWalletManager.getLastTransactionHash(currencyToSend.currency.network.backendId, derivationPath) ?: "", - ) - TxState.TxSent( - txAddress = userWalletManager.getLastTransactionHash( - networkId = currencyToSend.currency.network.backendId, - derivationPath = derivationPath - ) ?: "" - ) - }) + TxState.TxSent( + fromAmount = amountFormatter.formatSwapAmountToUI( + amount, + currencyToSend.currency.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + exchangeData.dataModel.toTokenAmount, + currencyToGet.currency.symbol, + ), + txAddress = userWalletManager.getLastTransactionHash( + currencyToSend.currency.network.backendId, + derivationPath, + ) ?: "", + ) + TxState.TxSent( + txAddress = userWalletManager.getLastTransactionHash( + networkId = currencyToSend.currency.network.backendId, + derivationPath = derivationPath, + ) ?: "", + ) + },) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 9c06a31f85..16fb6a7285 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -114,7 +114,7 @@ class SwapDomainModule { @SwapScope @Provides - fun provideDemoCardUseCase() : IsDemoCardUseCase { + fun provideDemoCardUseCase(): IsDemoCardUseCase { return IsDemoCardUseCase(config = DemoConfig()) } @@ -129,10 +129,9 @@ class SwapDomainModule { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade + walletManagersFacade = walletManagersFacade, ) } - } @Qualifier 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 61f1089d60..37ef409ce5 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 @@ -121,6 +121,8 @@ sealed interface SwapWarning { * @property priceImpact in format = 10 (means 10%) */ data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning + data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 76844c8cd2..a4fa64ab24 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -19,4 +19,5 @@ data class UiActions( val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, + val onBuyClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 78feaf99a9..2d1d66ef31 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.models.states +import com.tangem.core.ui.extensions.TextReference + sealed class ProviderState { abstract val onProviderClick: ((String) -> Unit)? @@ -32,7 +34,8 @@ sealed class ProviderState { val name: String, val type: String, val iconUrl: String, - val alertText: String, + val alertText: TextReference, + val selectionType: SelectionType, override val onProviderClick: ((String) -> Unit)? = null, ) : ProviderState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 6d2bab0f4f..c0743867bd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet 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.ProviderState @@ -99,7 +100,8 @@ private fun ChooseProviderBottomSheet_Preview() { name = "1inch", type = "DEX", iconUrl = "", - alertText = "Unavailable", + selectionType = ProviderState.SelectionType.SELECT, + alertText = stringReference("Unavailable"), ), ) TangemTheme(isDark = false) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 5d75293da0..8e51119147 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -13,12 +13,15 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ProviderState @@ -64,6 +67,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected ProviderUnavailableState( state = state, modifier = modifier, + isSelected = isSelected, ) } is ProviderState.Empty -> { @@ -127,31 +131,42 @@ private fun ProviderContentState( } } Row( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing56, + ), ) { Text( text = state.rate, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) if (state.percentLowerThenBest != null) { Text( - text = "${state.percentLowerThenBest}%", // todo add to strings + text = "${state.percentLowerThenBest}%", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.warning, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } } } } - ProviderChevron(state = state, isSelected = isSelected) + ProviderChevron(selectionType = state.selectionType, isSelected = isSelected) } } @Composable -private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: Modifier = Modifier) { +private fun ProviderUnavailableState( + state: ProviderState.Unavailable, + isSelected: Boolean, + modifier: Modifier = Modifier, +) { Box(modifier = modifier.fillMaxWidth()) { Row { val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter @@ -195,13 +210,15 @@ private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: ) } Text( - text = state.alertText, + text = state.alertText.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } } + + ProviderChevron(selectionType = state.selectionType, isSelected = isSelected) } } @@ -247,8 +264,8 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { } @Composable -private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: Boolean) { - when (state.selectionType) { +private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) { + when (selectionType) { ProviderState.SelectionType.NONE -> { /* no-op */ } @@ -396,7 +413,8 @@ private fun ProviderItem_Unavailable_Preview() { name = "1inch", type = "DEX", iconUrl = "", - alertText = "Unavailable", + selectionType = ProviderState.SelectionType.SELECT, + alertText = stringReference("Unavailable"), ) Column { TangemTheme(isDark = false) { @@ -408,5 +426,11 @@ private fun ProviderItem_Unavailable_Preview() { TangemTheme(isDark = true) { ProviderItemBlock(state = state) } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItem(state = state, isSelected = true) + } } } \ 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 054e89a4bc..dd313d462a 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 @@ -6,10 +6,7 @@ import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +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 @@ -125,7 +122,7 @@ internal class StateBuilder( notificationConfig = NotificationConfig( title = stringReference("No tokens"), subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.ic_alert_24, + iconResId = R.drawable.img_attention_20, ), ), ), @@ -213,6 +210,18 @@ internal class StateBuilder( ), ) } + if (quoteModel.permissionState !is PermissionDataState.PermissionReadyForRequest && + !quoteModel.preparedSwapConfigState.isFeeEnough + ) { + warnings.add( + SwapWarning.UnableToCoverFeeWarning( + createUnableToCoverFeeNotificationConfig( + fromToken = fromToken, + onBuyClick = actions.onBuyClick, + ), + ), + ) + } if (!quoteModel.preparedSwapConfigState.isBalanceEnough) { warnings.add(SwapWarning.InsufficientFunds) } @@ -605,14 +614,23 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, + bestRatedProviderId: String, providersStates: Map, + unavailableProviders: List, onDismiss: () -> Unit, ): SwapStateHolder { + val availableProvidersStates = providersStates.entries.mapNotNull { + it.convertToProviderState(bestRatedProviderId, actions.onProviderSelect) + } + val unavailableProviderStates = unavailableProviders.map { + it.convertToUnavailableProviderState( + alertText = stringReference("Unavailable for pair"), + selectionType = ProviderState.SelectionType.NONE, + ) + } val config = ChooseProviderBottomSheetConfig( selectedProviderId = selectedProviderId, - providers = providersStates.entries - .mapNotNull { it.convertToProviderState(actions.onProviderSelect) } - .toImmutableList(), + providers = (availableProvidersStates + unavailableProviderStates).toImmutableList(), ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -703,21 +721,27 @@ internal class StateBuilder( } private fun Map.Entry.convertToProviderState( + bestRatedProviderId: String, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key return when (val state = this.value) { is SwapState.EmptyAmountState -> null - is SwapState.QuotesLoadedState -> provider.convertToContentClickableProviderState( - fromTokenInfo = state.fromTokenInfo, - toTokenInfo = state.toTokenInfo, + is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState( + isBestRate = provider.providerId == bestRatedProviderId, + state = state, onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) - is SwapState.SwapError -> null + is SwapState.SwapError -> provider.convertToUnavailableProviderState( + alertText = stringReference("Available from"), + selectionType = ProviderState.SelectionType.NONE, + onProviderClick = onProviderSelect, + ) } } + // region warnings private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.swapping_permission_header), @@ -737,6 +761,25 @@ internal class StateBuilder( ) } + private fun createUnableToCoverFeeNotificationConfig( + fromToken: CryptoCurrency, + onBuyClick: () -> Unit, + ): NotificationConfig { + // todo add strings + return NotificationConfig( + title = stringReference("Unable to cover ${fromToken.name} fee"), + subtitle = stringReference( + "To make transaction you need to deposit some ${fromToken.name} ${fromToken.symbol}", + ), + iconResId = fromToken.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Buy ${fromToken.name}"), + onClick = onBuyClick, + ), + ) + } + // end region + private fun getShortAddressValue(fullAddress: String): String { check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) @@ -774,6 +817,58 @@ internal class StateBuilder( ) } + private fun SwapProvider.convertToContentSelectableProviderState( + isBestRate: Boolean, + state: SwapState.QuotesLoadedState, + selectionType: ProviderState.SelectionType, + onProviderClick: (String) -> Unit, + ): ProviderState { + val fromTokenInfo = state.fromTokenInfo + val toTokenInfo = state.toTokenInfo + val rate = toTokenInfo.tokenAmount.value.divide( + fromTokenInfo.tokenAmount.value, + toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol + val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol + val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { + ProviderState.AdditionalBadge.PermissionRequired + } else if (isBestRate) { + ProviderState.AdditionalBadge.BestTrade + } else { + ProviderState.AdditionalBadge.Empty + } + return ProviderState.Content( + id = this.providerId, + name = this.name, + iconUrl = this.imageLarge, + type = this.type.toString(), + rate = rateString, + additionalBadge = additionalBadge, + selectionType = selectionType, + percentLowerThenBest = null, + onProviderClick = onProviderClick, + ) + } + + private fun SwapProvider.convertToUnavailableProviderState( + alertText: TextReference, + selectionType: ProviderState.SelectionType, + onProviderClick: ((String) -> Unit)? = null, + ): ProviderState { + return ProviderState.Unavailable( + id = this.providerId, + name = this.name, + iconUrl = this.imageLarge, + type = this.type.toString(), + selectionType = selectionType, + alertText = alertText, + onProviderClick = onProviderClick, + ) + } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN 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 bf382cbc41..95fd4ce997 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 @@ -256,6 +256,7 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.HighPriceImpact -> { Notification( config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, ) } is SwapWarning.PermissionNeeded -> { @@ -282,6 +283,17 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.TooSmallAmountWarning -> { + Notification( + config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, + ) + } + is SwapWarning.UnableToCoverFeeWarning -> { + Notification( + config = warning.notificationConfig, + ) + } else -> {} } SpacerH8() @@ -379,7 +391,7 @@ private val state = SwapStateHolder( notificationConfig = NotificationConfig( title = stringReference("No tokens"), subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.ic_alert_24, + iconResId = R.drawable.img_attention_20, ), ), ), 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 d3121006ab..21c8322d64 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 @@ -17,7 +17,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -31,11 +30,14 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.isNullOrZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import java.math.BigDecimal +import java.math.RoundingMode import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -287,29 +289,62 @@ internal class SwapViewModel @Inject constructor( } is SwapState.SwapError -> { Timber.e("SwapError when loading quotes ${state.error}") + // todo handle when change token and error uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } } } } private fun updateLoadedQuotes(state: Map): Pair { - val selectedSwapProvider = selectProvider(state) + val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } + val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { + selectProvider(state) + } else { + null + } dataState = dataState.copy( selectedProvider = selectedSwapProvider, lastLoadedSwapStates = state, ) - return state.entries.first { it.key == selectedSwapProvider }.toPair() + selectedSwapProvider?.let { + return state.entries.first { it.key == selectedSwapProvider }.toPair() + } + return state.entries.first().toPair() } private fun selectProvider(state: Map): SwapProvider { - val currentSelected = dataState.selectedProvider - return if (currentSelected != null && state.keys.contains(currentSelected)) { - currentSelected + val stateSuccess = state + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + return if (stateSuccess.isNotEmpty()) { + val currentSelected = dataState.selectedProvider + if (currentSelected != null && state.keys.contains(currentSelected)) { + currentSelected + } else { + findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first() + } } else { state.keys.first() } } + private fun findBestQuoteProvider(state: Map): SwapProvider? { + // finding best quotes + return state.mapValues { + 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, + RoundingMode.HALF_UP, + ) + } else { + BigDecimal.ZERO + } + }.minByOrNull { it.value }?.key + } + private fun fillLoadedDataState( state: SwapState.QuotesLoadedState, permissionState: PermissionDataState, @@ -478,7 +513,6 @@ internal class SwapViewModel @Inject constructor( private fun onTokenSelect(id: String) { val tokens = dataState.tokensDataState ?: return - val foundToken = if (isOrderReversed) { tokens.fromGroup.available.firstOrNull { it.currencyStatus.currency.id.value == id @@ -505,6 +539,7 @@ internal class SwapViewModel @Inject constructor( dataState = dataState.copy( fromCryptoCurrency = fromToken, toCryptoCurrency = toToken, + selectedProvider = null, ) startLoadingQuotes( fromToken = fromToken, @@ -675,10 +710,17 @@ internal class SwapViewModel @Inject constructor( setupLoadedState(selectedProvider, updatedState, fromToken) } }, - onProviderClick = { + onProviderClick = { providerId -> + val states = dataState.lastLoadedSwapStates + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId + val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, - selectedProviderId = it, + selectedProviderId = providerId, + bestRatedProviderId = bestRatedProviderId, + unavailableProviders = unavailableProviders, providersStates = dataState.lastLoadedSwapStates, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -695,6 +737,7 @@ internal class SwapViewModel @Inject constructor( ) } }, + onBuyClick = {}, ) } @@ -736,6 +779,25 @@ internal class SwapViewModel @Inject constructor( return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList() } + private fun getAllProviders(): List { + dataState.tokensDataState?.let { data -> + val allProviders = + data.fromGroup.available.flatMap { it.providers } + data.toGroup.available.flatMap { it.providers } + return allProviders.distinct() + } ?: return emptyList() + } + + private fun getUnavailableProvidersFor(state: Map): List { + return getAllProviders() + .mapNotNull { + if (state.containsKey(it)) { + null + } else { + it + } + } + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = ""