Updated on 2026-08-14
This commit is contained in:
parent
d1798a2c49
commit
cf4affa513
16 changed files with 572 additions and 22 deletions
|
|
@ -1,7 +1,21 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
data class SwapSelectTokenStateHolder(
|
||||
val tokens: List<TokenToSelect>,
|
||||
val addedTokens: List<TokenToSelect>,
|
||||
val otherTokens: List<TokenToSelect>,
|
||||
val onSearchEntered: (String) -> Unit,
|
||||
val onTokenSelected: (String) -> Unit,
|
||||
)
|
||||
|
||||
data class TokenToSelect(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val iconUrl: String,
|
||||
val addedTokenBalanceData: TokenBalanceData? = null,
|
||||
)
|
||||
|
||||
data class TokenBalanceData(
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
)
|
||||
|
|
@ -22,18 +22,6 @@ data class SwapStateHolder(
|
|||
val onSuccess: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
data class TokenToSelect(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val addedTokenBalanceData: TokenBalanceData? = null,
|
||||
)
|
||||
|
||||
data class TokenBalanceData(
|
||||
val amount: String?,
|
||||
val amountEquivalent: String?,
|
||||
)
|
||||
|
||||
data class SwapCardData(
|
||||
val type: TransactionCardType,
|
||||
val amount: String?,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.fragment.app.viewModels
|
|||
import com.tangem.feature.swap.router.SwapRouter
|
||||
import com.tangem.feature.swap.router.SwapScreen
|
||||
import com.tangem.feature.swap.ui.SwapScreen
|
||||
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
|
||||
import com.tangem.feature.swap.ui.SwapSuccessScreen
|
||||
import com.tangem.feature.swap.viewmodels.SwapViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
@ -38,7 +39,18 @@ class SwapFragment : Fragment() {
|
|||
SwapScreen(stateHolder = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
SwapScreen.SelectToken -> SwapScreen(stateHolder = viewModel.uiState)// TODO: use proper screen
|
||||
SwapScreen.SelectToken -> {
|
||||
val tokenState = viewModel.uiState.selectTokenState
|
||||
if (tokenState != null) {
|
||||
SwapSelectTokenScreen(
|
||||
state = tokenState,
|
||||
onBack = viewModel.uiState.onBackClicked,
|
||||
)
|
||||
} else {
|
||||
SwapScreen(stateHolder = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
// screen
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
@file:OptIn(ExperimentalFoundationApi::class)
|
||||
|
||||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
|
||||
import com.tangem.core.ui.components.SpacerW2
|
||||
import com.tangem.core.ui.components.appbar.ExpandableSearchView
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenToSelect
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
@Composable
|
||||
fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
|
||||
|
||||
TangemTheme {
|
||||
Scaffold(
|
||||
content = {
|
||||
ListOfTokens(state = state)
|
||||
},
|
||||
topBar = {
|
||||
ExpandableSearchView(
|
||||
title = stringResource(R.string.swapping_token_list_your_title),
|
||||
onBackClick = onBack,
|
||||
placeholderSearchText = stringResource(id = R.string.search_tokens_title),
|
||||
onSearchChanged = state.onSearchEntered,
|
||||
onSearchDisplayClosed = { state.onSearchEntered("") },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListOfTokens(state: SwapSelectTokenStateHolder) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing32,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
) {
|
||||
|
||||
stickyHeader {
|
||||
Header(title = R.string.swapping_token_list_your_tokens)
|
||||
}
|
||||
|
||||
|
||||
itemsIndexed(items = state.addedTokens) { index, item ->
|
||||
TokenItem(token = item, onTokenClick = { state.onTokenSelected(item.id) })
|
||||
|
||||
if (index != state.addedTokens.lastIndex) Divider(
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
startIndent = TangemTheme.dimens.spacing54,
|
||||
)
|
||||
}
|
||||
|
||||
stickyHeader {
|
||||
Header(title = R.string.swapping_token_list_other_tokens)
|
||||
}
|
||||
|
||||
itemsIndexed(items = state.otherTokens) { index, item ->
|
||||
TokenItem(token = item, onTokenClick = { state.onTokenSelected(item.id) })
|
||||
if (index != state.otherTokens.lastIndex) Divider(
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
startIndent = TangemTheme.dimens.spacing54,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(@StringRes title: Int) {
|
||||
Text(
|
||||
text = stringResource(id = title).uppercase(),
|
||||
style = TangemTheme.typography.overline,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(vertical = TangemTheme.dimens.spacing6),
|
||||
textAlign = TextAlign.Start,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenItem(
|
||||
token: TokenToSelect,
|
||||
onTokenClick: (String) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.clickable(
|
||||
onClick = { onTokenClick(token.id) },
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing12)
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
val iconModifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
SubcomposeAsyncImage(
|
||||
modifier = iconModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(token.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = token.id,
|
||||
loading = { TokenImageShimmer(modifier = iconModifier) },
|
||||
error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = token.name,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerW2()
|
||||
Text(
|
||||
text = token.symbol,
|
||||
style = TangemTheme.typography.caption,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (token.addedTokenBalanceData != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = token.addedTokenBalanceData.amount ?: "",
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerW2()
|
||||
Text(
|
||||
text = token.addedTokenBalanceData.amountEquivalent ?: "",
|
||||
style = TangemTheme.typography.caption,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenImageShimmer(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.shimmer()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val token = TokenToSelect(
|
||||
id = "",
|
||||
name = "USDC",
|
||||
symbol = "USDC",
|
||||
iconUrl = "",
|
||||
addedTokenBalanceData = TokenBalanceData(
|
||||
amount = "15 000 $",
|
||||
amountEquivalent = "15 000 " +
|
||||
"USDT",
|
||||
),
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TokenScreenPreview() {
|
||||
SwapSelectTokenScreen(
|
||||
state = SwapSelectTokenStateHolder(
|
||||
listOf(token, token, token), listOf(token, token, token), {}, {},
|
||||
),
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -15,10 +16,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
|
|
@ -46,9 +47,9 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun TransactionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
type: TransactionCardType,
|
||||
amount: String?,
|
||||
amountEquivalent: String?,
|
||||
|
|
@ -56,15 +57,12 @@ fun TransactionCard(
|
|||
tokenCurrency: String,
|
||||
@DrawableRes networkIconRes: Int? = null,
|
||||
onChangeTokenClick: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
elevation = TangemTheme.dimens.elevation2,
|
||||
onClick = onChangeTokenClick ?: {},
|
||||
enabled = onChangeTokenClick != null,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
|
|
@ -96,6 +94,13 @@ fun TransactionCard(
|
|||
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
|
||||
ChangeTokenSelector()
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.height(TangemTheme.dimens.size116)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.clickable { onChangeTokenClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ import com.tangem.feature.swap.models.FeeState
|
|||
import com.tangem.feature.swap.models.SwapButton
|
||||
import com.tangem.feature.swap.models.SwapCardData
|
||||
import com.tangem.feature.swap.models.SwapPermissionStateHolder
|
||||
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.SwapSuccessStateHolder
|
||||
import com.tangem.feature.swap.models.SwapWarning
|
||||
import com.tangem.feature.swap.models.TokenBalanceData
|
||||
import com.tangem.feature.swap.models.TokenToSelect
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.router.SwapRouter
|
||||
|
|
@ -45,7 +48,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
swapRouter = router
|
||||
uiState = uiState.copy(
|
||||
onBackClicked = router::back,
|
||||
onSelectTokenClick = { router.openScreen(SwapScreen.Success) },
|
||||
onSelectTokenClick = { router.openScreen(SwapScreen.SelectToken) },
|
||||
onSuccess = { router.openScreen(SwapScreen.Success) },
|
||||
)
|
||||
}
|
||||
|
|
@ -108,6 +111,19 @@ internal class SwapViewModel @Inject constructor(
|
|||
cancelButton = CancelPermissionButton(true) {},
|
||||
)
|
||||
|
||||
val token = TokenToSelect(
|
||||
id = "USDC",
|
||||
name = "USDC",
|
||||
symbol = "USDC",
|
||||
iconUrl = "",
|
||||
addedTokenBalanceData = TokenBalanceData(amount = "15 000 $", amountEquivalent = "15 000 USDT"),
|
||||
)
|
||||
|
||||
val selectTokenState = SwapSelectTokenStateHolder(
|
||||
listOf(token, token, token, token, token, token, token, token, token),
|
||||
listOf(token, token, token, token, token, token, token, token, token), {}, {},
|
||||
)
|
||||
|
||||
return SwapStateHolder(
|
||||
sendCardData = sendCard,
|
||||
receiveCardData = receiveCard,
|
||||
|
|
@ -118,6 +134,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {},
|
||||
permissionState = permissionState,
|
||||
successState = SwapSuccessStateHolder("Success"),
|
||||
selectTokenState = selectTokenState,
|
||||
)
|
||||
}
|
||||
// endregion temp for test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue