Updated on 2026-08-14
This commit is contained in:
parent
03f003091e
commit
7f71ef182e
11 changed files with 260 additions and 61 deletions
|
|
@ -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.*
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ data class UiActions(
|
|||
val onSelectFeeType: (TxFee) -> Unit,
|
||||
val onProviderClick: (String) -> Unit,
|
||||
val onProviderSelect: (String) -> Unit,
|
||||
val onBuyClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapProvider, SwapState>,
|
||||
unavailableProviders: List<SwapProvider>,
|
||||
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<SwapProvider, SwapState>.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
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
is SwapWarning.HighPriceImpact -> {
|
||||
Notification(
|
||||
config = warning.notificationConfig,
|
||||
iconTint = TangemTheme.colors.icon.warning,
|
||||
)
|
||||
}
|
||||
is SwapWarning.PermissionNeeded -> {
|
||||
|
|
@ -282,6 +283,17 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, SwapState>): Pair<SwapProvider, SwapState> {
|
||||
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, SwapState>): 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, SwapState.QuotesLoadedState>): 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<SwapProvider> {
|
||||
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<SwapProvider, SwapState>): List<SwapProvider> {
|
||||
return getAllProviders()
|
||||
.mapNotNull {
|
||||
if (state.containsKey(it)) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val loggingTag = "SwapViewModel"
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue