Updated on 2026-08-14
This commit is contained in:
parent
8ef0c9f5a6
commit
6d697e05c1
15 changed files with 217 additions and 83 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ data class UiActions(
|
|||
val onGivePermissionClick: () -> Unit,
|
||||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onMaxAmountSelected: () -> Unit,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 $)"),
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue