Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-11 19:25:44 +03:00
commit ca612b304c
10 changed files with 87 additions and 69 deletions

View file

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

View file

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

View file

@ -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<ProviderState> {
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
}
}
}

View file

@ -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 = {},
),

View file

@ -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 = {},
)

View file

@ -789,7 +789,7 @@ internal class StateBuilder(
fun showSelectProviderBottomSheet(
uiState: SwapStateHolder,
selectedProviderId: String,
pricesLowerBest: Map<SwapProvider, Float>,
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
unavailableProviders: List<SwapProvider>,
onDismiss: () -> Unit,
@ -820,6 +820,7 @@ internal class StateBuilder(
fun updateProvidersBottomSheetContent(
uiState: SwapStateHolder,
pricesLowerBest: Map<String, Float>,
tokenSwapInfoForProviders: Map<String, TokenSwapInfo>,
): SwapStateHolder {
val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig
@ -835,6 +836,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 +932,7 @@ internal class StateBuilder(
}
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
pricesLowerBest: Map<SwapProvider, Float>,
pricesLowerBest: Map<String, Float>,
onProviderSelect: (String) -> Unit,
): ProviderState? {
val provider = this.key
@ -1040,7 +1044,7 @@ internal class StateBuilder(
subtitle = stringReference(rateString),
additionalBadge = badge,
selectionType = selectionType,
percentLowerThenBest = ZERO_PERCENT,
percentLowerThenBest = PercentLowerThanBest.Empty,
onProviderClick = onProviderClick,
)
}
@ -1049,7 +1053,7 @@ internal class StateBuilder(
isBestRate: Boolean,
state: SwapState.QuotesLoadedState,
selectionType: ProviderState.SelectionType,
pricesLowerBest: Map<SwapProvider, Float>,
pricesLowerBest: Map<String, Float>,
onProviderClick: (String) -> Unit,
): ProviderState {
val toTokenInfo = state.toTokenInfo
@ -1069,7 +1073,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 +1109,7 @@ internal class StateBuilder(
selectionType = selectionType,
subtitle = alertText,
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = ZERO_PERCENT,
percentLowerThenBest = PercentLowerThanBest.Empty,
onProviderClick = onProviderClick,
)
}
@ -1148,6 +1154,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
}
}

View file

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

View file

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

View file

@ -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() }

View file

@ -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
@ -289,11 +288,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 +385,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()
@ -869,21 +870,21 @@ internal class SwapViewModel @Inject constructor(
}?.key
}
private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map<SwapProvider, Float> {
private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map<String, Float> {
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<AppCurrency> {