diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 345f06a0f6..cbd1bf2dab 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -681,7 +681,7 @@ No available tokens to swap To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee - Service temporary unavailable + Service temporarily unavailable Please change the amount to swap The amount to swap must be at least %s This card might be a production sample or counterfeit diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt index 4baf118b61..a920fda100 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt @@ -1,13 +1,10 @@ package com.tangem.core.ui.components import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -25,23 +22,9 @@ sealed interface Keyboard { /** * Allows to subscribe to a soft keyboard to detect when it's open/closed */ -@Deprecated("Use Modifier.imePadding() on pure Compose screens (without XML layouts)") @Composable fun keyboardAsState(): State { - val density = LocalDensity.current - val imeInsets = WindowInsets.ime - .exclude(WindowInsets.navigationBars) - .getBottom(density) - - return remember(imeInsets) { - derivedStateOf { - if (imeInsets > 0) { - Keyboard.Opened( - height = with(density) { imeInsets.toDp() }, - ) - } else { - Keyboard.Closed - } - } - } + val bottom = WindowInsets.ime.getBottom(LocalDensity.current) + val isImeVisible = bottom > 0 + return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 87dee2d3d9..61962b7ca5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -118,12 +118,12 @@ class GetCryptoCurrencyActionsUseCase( val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() // swap - if (isMulticurrencyWallet && - marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency) - ) { - activeList.add(TokenActionsState.ActionState.Swap(true)) - } else { - disabledList.add(TokenActionsState.ActionState.Swap(false)) + if (isMulticurrencyWallet) { + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Swap(true)) + } else { + disabledList.add(TokenActionsState.ActionState.Swap(false)) + } } // buy @@ -164,11 +164,13 @@ class GetCryptoCurrencyActionsUseCase( return activeList + disabledList } - private fun isSendDisabled(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean = - cryptoCurrencyStatus.value.amount.isNullOrZero() || - coinStatus?.value?.amount.isNullOrZero() || - currenciesRepository.hasPendingTransactions( - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) + private fun isSendDisabled( + cryptoCurrencyStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus?, + ): Boolean = cryptoCurrencyStatus.value.amount.isNullOrZero() || + coinStatus?.value?.amount.isNullOrZero() || + currenciesRepository.hasPendingTransactions( + cryptoCurrencyStatus = cryptoCurrencyStatus, + coinStatus = coinStatus, + ) } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9e41c08329..26ac346f90 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi @@ -28,6 +29,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* @@ -62,49 +64,60 @@ internal class SwapRepositoryImpl @Inject constructor( currencyList: List, ): PairsWithProviders { return withContext(coroutineDispatcher.io) { - val initial = NetworkLeastTokenInfo( - contractAddress = initialCurrency.contractAddress, - network = initialCurrency.network, - ) - val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } - - val pairs = async { - getPairsInternal( - from = arrayListOf(initial), - to = currenciesList, + try { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, ) - } + val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } - val reversedPairs = async { - getPairsInternal( - from = currenciesList, - to = arrayListOf(initial), + val pairsDeferred = async { + getPairsInternal( + from = arrayListOf(initial), + to = currenciesList, + ) + } + + val reversedPairsDeferred = async { + getPairsInternal( + from = currenciesList, + to = arrayListOf(initial), + ) + } + + val pairs = pairsDeferred.await().getOrThrow() + val reversedPairs = reversedPairsDeferred.await().getOrThrow() + + val allPairs = pairs + reversedPairs + + val providers = tangemExpressApi.getProviders().getOrThrow() + + return@withContext swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = providers, + ), ) + } catch (exception: Exception) { + if (exception is ApiResponseError.HttpException) { + throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + } else { + throw exception + } } - - val allPairs = pairs.await() + reversedPairs.await() - - val providers = tangemExpressApi.getProviders().getOrThrow() - - return@withContext swapPairInfoConverter.convert( - SwapPairsWithProviders( - swapPair = allPairs, - providers = providers, - ), - ) } } private suspend fun getPairsInternal( from: List, to: List, - ): List { + ): ApiResponse> { return tangemExpressApi.getPairs( PairsRequestBody( from = from, to = to, ), - ).getOrThrow() + ) } override suspend fun getExchangeStatus(txId: String): Either { 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 f1af16641e..6504c6d53a 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 @@ -461,10 +461,12 @@ internal class SwapInteractorImpl @Inject constructor( amount, currencyToSend.symbol, ), + fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( swapData.toTokenAmount, currencyToGet.symbol, ), + toAmountValue = swapData.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) @@ -541,10 +543,12 @@ internal class SwapInteractorImpl @Inject constructor( amount, currencyToSend.currency.symbol, ), + fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( exchangeData.dataModel.toTokenAmount, currencyToGet.currency.symbol, ), + toAmountValue = exchangeData.dataModel.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt new file mode 100644 index 0000000000..cc416b82ca --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.swap.domain.models + +class ExpressException(val dataError: DataError) : Exception() \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt index a593888610..526b5b2a73 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt @@ -1,10 +1,14 @@ package com.tangem.feature.swap.domain.models.ui +import java.math.BigDecimal + sealed class TxState { data class TxSent( val fromAmount: String? = null, + val fromAmountValue: BigDecimal? = null, val toAmount: String? = null, + val toAmountValue: BigDecimal? = null, val txAddress: String, val txExternalUrl: String? = null, val timestamp: Long, 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 84254a5716..265631000e 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 @@ -25,7 +25,7 @@ sealed class ProviderState { val subtitle: TextReference, val selectionType: SelectionType, val additionalBadge: AdditionalBadge, - val percentLowerThenBest: Float = 0f, + val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -50,18 +50,35 @@ sealed class ProviderState { } } +sealed class PercentLowerThanBest { + data class Value(val value: Float) : PercentLowerThanBest() + object Empty : PercentLowerThanBest() +} + object ProviderPercentDiffComparator : Comparator { override fun compare(o1: ProviderState, o2: ProviderState): Int { if (o1 is ProviderState.Content && o2 !is ProviderState.Content) { - return 1 - } - if (o1 !is ProviderState.Content && o2 is ProviderState.Content) { return -1 } - return if (o1 is ProviderState.Content && o2 is ProviderState.Content) { - o1.percentLowerThenBest.compareTo(o2.percentLowerThenBest) + if (o1 !is ProviderState.Content && o2 is ProviderState.Content) { + return 1 + } + if (o1 is ProviderState.Content && o2 is ProviderState.Content) { + val o1Percent = o1.percentLowerThenBest + val o2Percent = o2.percentLowerThenBest + if (o1Percent is PercentLowerThanBest.Value && o2Percent !is PercentLowerThanBest.Value) { + return -1 + } + if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { + return 1 + } + return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { + o1Percent.value.compareTo(o2Percent.value) + } else { + 0 + } } else { - 0 + return 0 } } } \ No newline at end of file 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 2dd33c473f..363085e36c 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 @@ -17,6 +17,7 @@ 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.PercentLowerThanBest import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList @@ -89,7 +90,7 @@ private fun ChooseProviderBottomSheet_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.BestTrade, - percentLowerThenBest = -1.0f, + percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ), 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 e5fe4ef2c9..51f6936885 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 @@ -24,6 +24,7 @@ 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.PercentLowerThanBest import com.tangem.feature.swap.models.states.ProviderState /** @@ -151,8 +152,10 @@ private fun ProviderContentState( maxLines = 1, ) } - if (state.percentLowerThenBest > 0f) { - AnimatedContent(targetState = state.percentLowerThenBest, label = "") { + if (state.percentLowerThenBest is PercentLowerThanBest.Value && + state.percentLowerThenBest.value > 0 + ) { + AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { Text( text = "-$it%", style = TangemTheme.typography.body2, @@ -404,7 +407,7 @@ private fun ProviderItem_Content_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, - percentLowerThenBest = -1.0f, + percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ) 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 bb1842d035..6881df60df 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 @@ -579,14 +579,14 @@ internal class StateBuilder( } } - fun createInitialErrorState(uiState: SwapStateHolder, onRefreshClick: () -> Unit): SwapStateHolder { + fun createInitialErrorState(uiState: SwapStateHolder, code: Int, onRefreshClick: () -> Unit): SwapStateHolder { return uiState.copy( warnings = listOf( SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.EMPTY, - iconResId = R.drawable.ic_alert_circle_24, + subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)), + iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = TextReference.Res(R.string.warning_button_refresh), onClick = onRefreshClick, @@ -639,7 +639,7 @@ internal class StateBuilder( @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, - timeStamp: Long, + txState: TxState.TxSent, txUrl: String, dataState: SwapProcessDataState, onExploreClick: () -> Unit, @@ -648,18 +648,16 @@ internal class StateBuilder( val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) - val fromAmount = toBigDecimalOrNull(requireNotNull(dataState.amount)) - val toAmount = requireNotNull(dataState.swapDataModel?.toTokenAmount?.value) + val fromAmount = txState.fromAmountValue ?: BigDecimal.ZERO + val toAmount = txState.toAmountValue ?: BigDecimal.ZERO val providerState = uiState.providerState as ProviderState.Content - val fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(fromAmount, fromCryptoCurrency.currency) - val toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(toAmount, toCryptoCurrency.currency) val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = timeStamp, + timestamp = txState.timestamp, txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), @@ -667,8 +665,8 @@ internal class StateBuilder( providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), - fromTokenAmount = stringReference(fromCryptoAmount), - toTokenAmount = stringReference(toCryptoAmount), + fromTokenAmount = stringReference(txState.fromAmount.orEmpty()), + toTokenAmount = stringReference(txState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), toTokenFiatAmount = stringReference(toFiatAmount), fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), @@ -789,7 +787,7 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, - pricesLowerBest: Map, + pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, onDismiss: () -> Unit, @@ -820,6 +818,7 @@ internal class StateBuilder( fun updateProvidersBottomSheetContent( uiState: SwapStateHolder, + pricesLowerBest: Map, tokenSwapInfoForProviders: Map, ): SwapStateHolder { val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig @@ -835,6 +834,9 @@ internal class StateBuilder( .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) it.copy( subtitle = stringReference(rateString), + percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> + PercentLowerThanBest.Value(percent) + } ?: PercentLowerThanBest.Empty, ) } else { it @@ -928,7 +930,7 @@ internal class StateBuilder( } private fun Map.Entry.convertToProviderBottomSheetState( - pricesLowerBest: Map, + pricesLowerBest: Map, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key @@ -1040,7 +1042,7 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, - percentLowerThenBest = ZERO_PERCENT, + percentLowerThenBest = PercentLowerThanBest.Empty, onProviderClick = onProviderClick, ) } @@ -1049,7 +1051,7 @@ internal class StateBuilder( isBestRate: Boolean, state: SwapState.QuotesLoadedState, selectionType: ProviderState.SelectionType, - pricesLowerBest: Map, + pricesLowerBest: Map, onProviderClick: (String) -> Unit, ): ProviderState { val toTokenInfo = state.toTokenInfo @@ -1069,7 +1071,9 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = additionalBadge, selectionType = selectionType, - percentLowerThenBest = pricesLowerBest[this] ?: ZERO_PERCENT, + percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent -> + PercentLowerThanBest.Value(percent) + } ?: PercentLowerThanBest.Value(0f), onProviderClick = onProviderClick, ) } @@ -1103,7 +1107,7 @@ internal class StateBuilder( selectionType = selectionType, subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, - percentLowerThenBest = ZERO_PERCENT, + percentLowerThenBest = PercentLowerThanBest.Empty, onProviderClick = onProviderClick, ) } @@ -1148,6 +1152,5 @@ internal class StateBuilder( private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" private const val MAX_DECIMALS_TO_SHOW = 8 - private const val ZERO_PERCENT = 0f } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index a70b6ce6f2..c25ea64c0b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -2,21 +2,33 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig +import com.tangem.feature.swap.presentation.R @Composable internal fun SwapScreen(stateHolder: SwapStateHolder) { BackHandler(onBack = stateHolder.onBackClicked) Scaffold( + modifier = Modifier.systemBarsPadding(), + topBar = { + AppBarWithBackButton( + text = stringResource(R.string.common_swap), + onBackClick = stateHolder.onBackClicked, + iconRes = R.drawable.ic_close_24, + ) + }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> 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 949ead0f64..ace1d19a31 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 @@ -17,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.getActiveIconResByCoinId @@ -40,12 +39,6 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi .background(color = TangemTheme.colors.background.secondary), ) { Column { - AppBarWithBackButton( - text = stringResource(R.string.common_swap), - onBackClick = state.onBackClicked, - iconRes = R.drawable.ic_close_24, - ) - Column( modifier = Modifier .fillMaxWidth() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 82b05a7c86..4057aba9af 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -97,7 +97,8 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = LazyColumn( modifier = modifier .background(color = screenBackgroundColor) - .fillMaxSize(), + .fillMaxSize() + .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { item { SpacerH8() } 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 101200169e..ac172210c3 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 @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.getOrElse -import arrow.core.mapNotNull import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter @@ -19,6 +18,7 @@ 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.DataError +import com.tangem.feature.swap.domain.models.ExpressException 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 @@ -175,13 +175,19 @@ internal class SwapViewModel @Inject constructor( selectedCurrency = null, ) - uiState = stateBuilder.createInitialErrorState(uiState) { - 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() + } } } } @@ -289,11 +295,13 @@ internal class SwapViewModel @Inject constructor( if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) + val successStates = providersState + .getLastLoadedSuccessStates() + val pricesLowerBest = getPricesLowerBest(successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, - tokenSwapInfoForProviders = providersState - .getLastLoadedSuccessStates() - .entries + pricesLowerBest = pricesLowerBest, + tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { @@ -384,7 +392,7 @@ internal class SwapViewModel @Inject constructor( val stateSuccess = state.getLastLoadedSuccessStates() return if (stateSuccess.isNotEmpty()) { val currentSelected = dataState.selectedProvider - if (currentSelected != null && state.keys.contains(currentSelected)) { + if (currentSelected != null && stateSuccess.keys.contains(currentSelected)) { currentSelected } else { findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first() @@ -457,7 +465,7 @@ internal class SwapViewModel @Inject constructor( ) uiState = stateBuilder.createSuccessState( uiState = uiState, - timeStamp = it.timestamp, + txState = it, dataState = dataState, txUrl = url, onExploreClick = { @@ -869,21 +877,21 @@ internal class SwapViewModel @Inject constructor( }?.key } - private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { + private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap() val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value val hundredPercent = BigDecimal("100") - return state.mapNotNull { + return state.entries.mapNotNull { if (it.key != bestRateEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( amount.divide(bestRate, RoundingMode.HALF_UP), ).multiply(hundredPercent) - percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue + it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue } else { null } - } + }.toMap() } private fun createSelectedAppCurrencyFlow(): StateFlow { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index edc76744e7..0df8cabe1b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-412" +tangemBlockchainSdk = "release-app_5.4-416" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^