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 = ""