Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-25 14:16:33 +03:00
parent 8ef0c9f5a6
commit 6d697e05c1
15 changed files with 217 additions and 83 deletions

View file

@ -6,6 +6,7 @@ data class SwapStateHolder(
val sendCardData: SwapCardData,
val receiveCardData: SwapCardData,
val networkCurrency: String,
val networkId: String,
val fee: FeeState = FeeState.Empty,
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,
@ -29,8 +30,8 @@ data class SwapCardData(
val type: TransactionCardType,
val amount: String?,
val amountEquivalent: String?,
val coinId: String?,
val tokenIconUrl: String,
val tokenId: String,
val tokenCurrency: String,
val balance: String,
@DrawableRes val networkIconRes: Int? = null,

View file

@ -8,4 +8,5 @@ data class UiActions(
val onGivePermissionClick: () -> Unit,
val onChangeCardsClicked: () -> Unit,
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
)

View file

@ -29,13 +29,14 @@ class StateBuilder(val actions: UiActions) {
fun createInitialLoadingState(initialCurrency: Currency): SwapStateHolder {
return SwapStateHolder(
networkId = initialCurrency.networkId,
sendCardData = SwapCardData(
type = TransactionCardType.SendCard(actions.onAmountChanged),
amount = null,
amountEquivalent = null,
tokenIconUrl = initialCurrency.logoUrl,
tokenCurrency = initialCurrency.symbol,
tokenId = initialCurrency.id,
coinId = initialCurrency.id,
canSelectAnotherToken = false,
balance = "",
),
@ -47,7 +48,7 @@ class StateBuilder(val actions: UiActions) {
tokenCurrency = "",
canSelectAnotherToken = false,
balance = "",
tokenId = "",
coinId = null,
),
fee = FeeState.Loading,
networkCurrency = initialCurrency.symbol,
@ -55,6 +56,8 @@ class StateBuilder(val actions: UiActions) {
onRefresh = {},
onBackClicked = actions.onBackClicked,
onChangeCardsClicked = actions.onChangeCardsClicked,
onMaxAmountSelected = actions.onMaxAmountSelected,
updateInProgress = true,
)
}
@ -68,10 +71,10 @@ class StateBuilder(val actions: UiActions) {
sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amount = uiStateHolder.sendCardData.amount,
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
amountEquivalent = null,
tokenIconUrl = fromToken.logoUrl,
tokenCurrency = fromToken.symbol,
tokenId = fromToken.id,
coinId = fromToken.id,
canSelectAnotherToken = mainTokenId != fromToken.id,
balance = "",
),
@ -81,13 +84,14 @@ class StateBuilder(val actions: UiActions) {
amountEquivalent = null,
tokenIconUrl = toToken.logoUrl,
tokenCurrency = toToken.symbol,
tokenId = toToken.id,
coinId = toToken.id,
canSelectAnotherToken = mainTokenId != toToken.id,
balance = "",
),
fee = FeeState.Loading,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
permissionState = uiStateHolder.permissionState,
updateInProgress = true,
)
}
@ -111,10 +115,10 @@ class StateBuilder(val actions: UiActions) {
return uiStateHolder.copy(
sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amount = quoteModel.fromTokenInfo.tokenAmount.formatToUIRepresentation(),
amount = uiStateHolder.sendCardData.amount,
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
tokenId = quoteModel.fromTokenInfo.tokenId,
coinId = quoteModel.fromTokenInfo.coinId,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
@ -124,7 +128,7 @@ class StateBuilder(val actions: UiActions) {
amount = quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation(),
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
tokenId = quoteModel.toTokenInfo.tokenId,
coinId = quoteModel.toTokenInfo.coinId,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
balance = quoteModel.toTokenInfo.tokenWalletBalance,
@ -140,6 +144,7 @@ class StateBuilder(val actions: UiActions) {
loading = false,
onClick = actions.onSwapClick,
),
updateInProgress = false,
)
}
@ -151,9 +156,9 @@ class StateBuilder(val actions: UiActions) {
sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amount = uiStateHolder.sendCardData.amount,
amountEquivalent = uiStateHolder.sendCardData.amountEquivalent,
amountEquivalent = "",
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
tokenId = uiStateHolder.sendCardData.tokenId,
coinId = uiStateHolder.sendCardData.coinId,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = emptyAmountState.fromTokenWalletBalance,
@ -161,9 +166,9 @@ class StateBuilder(val actions: UiActions) {
receiveCardData = SwapCardData(
type = TransactionCardType.ReceiveCard(),
amount = "0",
amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent,
amountEquivalent = "",
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
tokenId = uiStateHolder.receiveCardData.tokenId,
coinId = uiStateHolder.receiveCardData.coinId,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
balance = emptyAmountState.toTokenWalletBalance,
@ -174,6 +179,7 @@ class StateBuilder(val actions: UiActions) {
loading = false,
onClick = { },
),
updateInProgress = false,
)
}
@ -251,6 +257,7 @@ class StateBuilder(val actions: UiActions) {
message = null,
onClick = onAlertClick,
),
updateInProgress = false,
)
}
@ -302,4 +309,10 @@ class StateBuilder(val actions: UiActions) {
const val ADDRESS_FIRST_PART_LENGTH = 7
const val ADDRESS_SECOND_PART_LENGTH = 4
}
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
updateInProgress = true,
)
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@ -34,6 +35,10 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
)
} else {
// Required "else" block to prevent compose crash
// always close BS if its empty, cause user should not see this
LaunchedEffect(Unit) {
coroutineScope.launch { bottomSheetState.hide() }
}
Box(modifier = Modifier.fillMaxSize())
}
},

View file

@ -39,7 +39,7 @@ import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getActiveIconResByCoinId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.SwapButton
@ -137,7 +137,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.button.secondary)
.clickable { state.onMaxAmountSelected }
.clickable { state.onMaxAmountSelected?.invoke() }
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing16,
@ -170,7 +170,9 @@ private fun MainInfo(state: SwapStateHolder) {
tokenIconUrl = state.sendCardData.tokenIconUrl,
tokenCurrency = state.sendCardData.tokenCurrency,
networkIconRes = state.sendCardData.networkIconRes,
iconPlaceholder = getActiveIconRes(state.sendCardData.tokenId),
iconPlaceholder = state.sendCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
)
SpacerH16()
@ -182,7 +184,9 @@ private fun MainInfo(state: SwapStateHolder) {
tokenIconUrl = state.receiveCardData.tokenIconUrl,
tokenCurrency = state.receiveCardData.tokenCurrency,
networkIconRes = state.receiveCardData.networkIconRes,
iconPlaceholder = getActiveIconRes(state.receiveCardData.tokenId),
iconPlaceholder = state.receiveCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
state.onSelectTokenClick
} else {
@ -190,13 +194,13 @@ private fun MainInfo(state: SwapStateHolder) {
},
)
}
SwapButton()
SwapButton(state)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SwapButton() {
private fun SwapButton(state: SwapStateHolder) {
Card(
elevation = TangemTheme.dimens.elevation3,
shape = CircleShape,
@ -313,7 +317,7 @@ private val sendCard = SwapCardData(
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = false,
balance = "123",
tokenId = "",
coinId = "",
)
private val receiveCard = SwapCardData(
@ -325,10 +329,11 @@ private val receiveCard = SwapCardData(
networkIconRes = R.drawable.img_polygon_22,
canSelectAnotherToken = true,
balance = "33333",
tokenId = "",
coinId = "",
)
private val state = SwapStateHolder(
networkId = "ethereum",
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)"),

View file

@ -351,13 +351,14 @@ fun Token(
.align(Alignment.BottomStart)
.size(TangemTheme.dimens.size36)
val data = tokenIconUrl.ifEmpty {
iconPlaceholder
}
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(tokenIconUrl)
.data(data)
.crossfade(true)
.error(iconPlaceholder ?: 0)
.placeholder(iconPlaceholder ?: 0)
.build(),
loading = { TokenImageShimmer(modifier = tokenImageModifier) },
// error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) },

View file

@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
@ -23,6 +24,7 @@ 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.toFormattedString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
@ -125,6 +127,7 @@ internal class SwapViewModel @Inject constructor(
return PeriodicTask(
UPDATE_DELAY,
task = {
uiState = stateBuilder.createSilentLoadState(uiState)
runCatching(dispatchers.io) {
dataState = dataState.copy(
amount = amount,
@ -244,7 +247,7 @@ internal class SwapViewModel @Inject constructor(
private fun onSearchEntered(searchQuery: String) {
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.onSearchToken(dataState.networkId, searchQuery)
swapInteractor.searchTokens(dataState.networkId, searchQuery)
}
.onSuccess {
updateTokensState(it)
@ -283,22 +286,36 @@ internal class SwapViewModel @Inject constructor(
toCurrency = newToToken,
)
isOrderReversed = !isOrderReversed
lastAmount.value = cutAmountWithDecimals(swapInteractor.getTokenDecimals(newFromToken), lastAmount.value)
uiState = stateBuilder.updateSwapAmount(uiState, lastAmount.value)
startLoadingQuotes(newFromToken, newToToken, lastAmount.value)
}
}
private fun onAmountChanged(value: String) {
uiState = stateBuilder.updateSwapAmount(uiState, value)
lastAmount.value = value
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
val fromToken = dataState.fromCurrency
val toToken = dataState.toCurrency
if (fromToken != null && toToken != null) {
val fromToken = dataState.fromCurrency
val toToken = dataState.toCurrency
if (fromToken != null && toToken != null) {
val cutValue = cutAmountWithDecimals(swapInteractor.getTokenDecimals(fromToken), value)
uiState = stateBuilder.updateSwapAmount(uiState, cutValue)
lastAmount.value = cutValue
amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
startLoadingQuotes(fromToken, toToken, lastAmount.value)
}
}
}
private fun onMaxAmountClicked() {
dataState.fromCurrency?.let {
val balance = swapInteractor.getTokenBalance(it)
onAmountChanged(balance.formatToUIRepresentation())
}
}
private fun cutAmountWithDecimals(maxDecimals: Int, amount: String): String {
return amount.toBigDecimalOrNull()?.toFormattedString(maxDecimals) ?: INITIAL_AMOUNT
}
private fun createUiActions(): UiActions {
return UiActions(
onSearchEntered = { onSearchEntered(it) },
@ -308,12 +325,13 @@ internal class SwapViewModel @Inject constructor(
onGivePermissionClick = { givePermissionsToSwap() },
onChangeCardsClicked = { onChangeCardsClicked() },
onBackClicked = { onSearchEntered("") },
onMaxAmountSelected = { onMaxAmountClicked() },
)
}
companion object {
private const val INITIAL_AMOUNT = ""
private const val UPDATE_DELAY = 60000L
private const val UPDATE_DELAY = 10000L
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
}
}