Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-30 16:45:13 +04:00
commit b166701147
15 changed files with 344 additions and 198 deletions

View file

@ -18,6 +18,7 @@ object Compose {
const val material = "androidx.compose.material:material:" + Versions.compose
const val ui = "androidx.compose.ui:ui:" + Versions.compose
const val uiTooling = "androidx.compose.ui:ui-tooling:" + Versions.compose
const val constraintLayout = "androidx.constraintlayout:constraintlayout-compose:" + Versions.composeConstraint
}
object Firebase {

View file

@ -20,6 +20,7 @@ object Versions {
// region Compose
const val compose = "1.3.1"
const val composeFoundation = "1.3.1"
const val composeConstraint = "1.0.1"
// endregion Compose
// region Other libraries
@ -60,7 +61,7 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-151"
const val tangemBlockchainSdk = "develop-154"
// const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-180"

View file

@ -1,10 +1,13 @@
package com.tangem.core.ui.components.appbar
import androidx.compose.animation.Crossfade
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -26,6 +29,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
@ -40,13 +44,15 @@ import com.tangem.core.ui.res.TangemTheme
/**
* App bar with close icon and search functionality
*
* @param title optional title
* @param title optional title
* @param placeholderSearchText optional text placeholder for search TextField
* @param expandedInitially whether the search is expanded on launch
* @param tint tint for most of the visual elements of toolbar
* @param onBackClick action when close button is clicked
* @param onSearchChange action when search is modified
* @param onSearchDisplayClose action when search is closed
* @param subtitle additional text under the title (optional)
* @param icon additional icon under the title (optional)
*
* @see <a href =
* "https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=1123%3A4068&t=xj8BBj5DfCWn2Mli-1"
@ -61,6 +67,8 @@ fun ExpandableSearchView(
placeholderSearchText: String = "",
expandedInitially: Boolean = false,
tint: Color = TangemTheme.colors.icon.primary1,
subtitle: String? = null,
icon: Painter? = null,
) {
val (expanded, onExpandedChanged) = remember { mutableStateOf(expandedInitially) }
@ -79,6 +87,8 @@ fun ExpandableSearchView(
onBackClick = onBackClick,
onExpandedChange = onExpandedChanged,
tint = tint,
subtitle = subtitle,
icon = icon,
)
}
}
@ -90,11 +100,12 @@ private fun CollapsedSearchView(
onBackClick: () -> Unit,
onExpandedChange: (Boolean) -> Unit,
title: String? = null,
subtitle: String? = null,
icon: Painter? = null,
) {
Row(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
@ -103,25 +114,59 @@ private fun CollapsedSearchView(
painter = painterResource(R.drawable.ic_close_24),
contentDescription = null,
modifier = Modifier
.padding(TangemTheme.dimens.spacing16)
.size(TangemTheme.dimens.size24)
.clickable { onBackClick() },
tint = MaterialTheme.colors.onPrimary,
)
if (!title.isNullOrBlank()) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
) {
if (!title.isNullOrBlank()) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size24),
)
}
if (subtitle != null) {
SubtitleView(subtitle = subtitle, icon = icon)
}
}
SpacerWMax()
Icon(
painter = painterResource(id = R.drawable.ic_search_24),
tint = tint,
contentDescription = null,
modifier = Modifier
.clickable { onExpandedChange(true) },
.clickable { onExpandedChange(true) }
.padding(end = TangemTheme.dimens.spacing16),
)
}
}
@Composable
private fun SubtitleView(subtitle: String, icon: Painter?) {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Image(
painter = icon,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size16),
)
}
Text(
text = subtitle,
color = TangemTheme.colors.text.secondary,
maxLines = 1,
style = TangemTheme.typography.caption,
)
}
}
@ -200,6 +245,8 @@ private fun CollapsedSearchViewPreview() {
placeholderSearchText = "Search",
onSearchChange = {},
onSearchDisplayClose = {},
subtitle = "Ethereum",
icon = painterResource(id = R.drawable.img_eth_22),
)
}
}

View file

@ -266,7 +266,7 @@ internal class SwapInteractorImpl @Inject constructor(
appCurrency: ProxyFiatCurrency,
): List<TokenWithBalance> {
return tokens.map {
val balance = balances[it.id]
val balance = balances[it.symbol]
TokenWithBalance(
token = it,
tokenBalanceData = TokenBalanceData(

View file

@ -65,6 +65,7 @@ dependencies {
implementation(Compose.material)
implementation(Compose.uiTooling)
implementation(Compose.coil)
implementation(Compose.constraintLayout)
/** Domain */
implementation(project(":features:swap:domain"))

View file

@ -1,23 +1,26 @@
package com.tangem.feature.swap.converters
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
import com.tangem.feature.swap.models.Network
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
import com.tangem.utils.converter.Converter
class TokensDataConverter(
private val onSearchEntered: (String) -> Unit,
private val onTokenSelected: (String) -> Unit,
) : Converter<FoundTokensState, SwapSelectTokenStateHolder> {
) {
override fun convert(value: FoundTokensState): SwapSelectTokenStateHolder {
fun convertWithNetwork(value: FoundTokensState, network: NetworkInfo): SwapSelectTokenStateHolder {
return SwapSelectTokenStateHolder(
addedTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) },
otherTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) },
onSearchEntered = onSearchEntered,
onTokenSelected = onTokenSelected,
network = Network(network.name, network.blockchainId),
)
}
@ -27,6 +30,7 @@ class TokensDataConverter(
name = tokenWithBalance.token.name,
symbol = tokenWithBalance.token.symbol,
iconUrl = tokenWithBalance.token.logoUrl,
isNative = tokenWithBalance.token is Currency.NativeToken,
addedTokenBalanceData = TokenBalanceData(
amount = tokenWithBalance.tokenBalanceData?.amount,
amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.models
data class SwapSelectTokenStateHolder(
val addedTokens: List<TokenToSelect>,
val otherTokens: List<TokenToSelect>,
val network: Network,
val onSearchEntered: (String) -> Unit,
val onTokenSelected: (String) -> Unit,
)
@ -12,10 +13,16 @@ data class TokenToSelect(
val name: String,
val symbol: String,
val iconUrl: String,
val isNative: Boolean,
val available: Boolean = true,
val addedTokenBalanceData: TokenBalanceData? = null,
)
data class Network(
val name: String,
val blockchainId: String,
)
data class TokenBalanceData(
val amount: String?,
val amountEquivalent: String?,

View file

@ -62,6 +62,7 @@ sealed interface TransactionCardType {
data class SendCard(
val onAmountChanged: ((String) -> Unit),
val onFocusChanged: ((Boolean) -> Unit),
) : TransactionCardType
data class ReceiveCard(

View file

@ -4,6 +4,7 @@ data class UiActions(
val onSearchEntered: (String) -> Unit,
val onTokenSelected: (String) -> Unit,
val onAmountChanged: (String) -> Unit,
val onAmountSelected: (Boolean) -> Unit,
val onSwapClick: () -> Unit,
val onGivePermissionClick: () -> Unit,
val onChangeCardsClicked: () -> Unit,

View file

@ -0,0 +1,96 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.res.TangemTheme
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun AutoSizeTextField(
amount: String,
onAmountChange: (String) -> Unit,
onFocusChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
var shrunkFontSize = TangemTheme.typography.h2.fontSize
val calculateIntrinsics = @Composable {
ParagraphIntrinsics(
text = amount,
style = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
shrunkFontSize *= 0.9f
intrinsics = calculateIntrinsics()
}
}
val customTextSelectionColors = TextSelectionColors(
handleColor = Color.Transparent,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
)
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = amount,
onValueChange = onAmountChange,
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester)
.onFocusChanged { onFocusChange(it.hasFocus) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
decorationBox = { innerTextField ->
if (amount.isBlank()) {
Text(
text = "0",
color = TangemTheme.colors.text.disabled,
style = TangemTheme.typography.h2,
)
}
innerTextField()
},
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
)
}
}
}

View file

@ -34,7 +34,7 @@ internal class StateBuilder(val actions: UiActions) {
networkId = initialCurrency.networkId,
blockchainId = networkInfo.blockchainId,
sendCardData = SwapCardData(
type = TransactionCardType.SendCard(actions.onAmountChanged),
type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected),
amount = null,
amountEquivalent = null,
tokenIconUrl = initialCurrency.logoUrl,
@ -205,8 +205,17 @@ internal class StateBuilder(val actions: UiActions) {
)
}
fun addTokensToState(uiState: SwapStateHolder, dataState: FoundTokensState): SwapStateHolder {
return uiState.copy(selectTokenState = tokensDataConverter.convert(dataState))
fun addTokensToState(
uiState: SwapStateHolder,
dataState: FoundTokensState,
networkInfo: NetworkInfo,
): SwapStateHolder {
return uiState.copy(
selectTokenState = tokensDataConverter.convertWithNetwork(
value = dataState,
network = networkInfo,
),
)
}
private fun convertPermissionState(

View file

@ -28,8 +28,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.components.CardWithIcon
import com.tangem.core.ui.components.ClickableWarningCard
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconRight
@ -37,7 +37,7 @@ import com.tangem.core.ui.components.RefreshableWaringCard
import com.tangem.core.ui.components.SimpleOkDialog
import com.tangem.core.ui.components.SmallInfoCard
import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.WarningCard
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.getActiveIconRes
@ -85,12 +85,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
FeeItem(feeState = state.fee, currency = state.networkCurrency)
if (state.warnings.isNotEmpty()) {
SwapWarnings(
warnings = state.warnings,
onPermissionWarningClick = onPermissionWarningClick,
)
}
if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings)
if (state.permissionState is SwapPermissionState.InProgress) {
CardWithIcon(
@ -106,25 +101,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
},
)
}
if (state.warnings.any { it is SwapWarning.InsufficientFunds }) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_insufficient_funds),
enabled = false,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
} else {
PrimaryButtonIconRight(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_swap),
icon = painterResource(id = R.drawable.ic_tangem_24),
enabled = state.swapButton.enabled,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
}
MainButton(state = state, onPermissionWarningClick = onPermissionWarningClick)
}
}
@ -159,59 +136,71 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
@Composable
private fun MainInfo(state: SwapStateHolder) {
Box(
ConstraintLayout(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val networkIconRes = remember {
getActiveIconRes(state.blockchainId)
}
Column {
TransactionCard(
type = state.sendCardData.type,
balance = state.sendCardData.balance,
amount = state.sendCardData.amount,
amountEquivalent = state.sendCardData.amountEquivalent,
tokenIconUrl = state.sendCardData.tokenIconUrl,
tokenCurrency = state.sendCardData.tokenCurrency,
networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.sendCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
)
SpacerH16()
TransactionCard(
type = state.receiveCardData.type,
balance = state.receiveCardData.balance,
amount = state.receiveCardData.amount,
amountEquivalent = state.receiveCardData.amountEquivalent,
tokenIconUrl = state.receiveCardData.tokenIconUrl,
tokenCurrency = state.receiveCardData.tokenCurrency,
networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.receiveCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
state.onSelectTokenClick
} else {
null
},
)
}
SwapButton(state)
val (topCard, bottomCard, button) = createRefs()
TransactionCard(
type = state.sendCardData.type,
balance = state.sendCardData.balance,
amount = state.sendCardData.amount,
amountEquivalent = state.sendCardData.amountEquivalent,
tokenIconUrl = state.sendCardData.tokenIconUrl,
tokenCurrency = state.sendCardData.tokenCurrency,
networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.sendCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null,
modifier = Modifier.constrainAs(topCard) {
top.linkTo(parent.top)
},
)
val marginCard = TangemTheme.dimens.spacing16
TransactionCard(
type = state.receiveCardData.type,
balance = state.receiveCardData.balance,
amount = state.receiveCardData.amount,
amountEquivalent = state.receiveCardData.amountEquivalent,
tokenIconUrl = state.receiveCardData.tokenIconUrl,
tokenCurrency = state.receiveCardData.tokenCurrency,
networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null,
iconPlaceholder = state.receiveCardData.coinId?.let {
getActiveIconResByCoinId(it, state.networkId)
},
onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) {
state.onSelectTokenClick
} else {
null
},
modifier = Modifier.constrainAs(bottomCard) {
top.linkTo(topCard.bottom, margin = marginCard)
},
)
val marginButton = TangemTheme.dimens.spacing32
SwapButton(
state,
modifier = Modifier.constrainAs(button) {
bottom.linkTo(topCard.bottom, margin = -marginButton)
start.linkTo(topCard.start)
end.linkTo(topCard.end)
},
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SwapButton(state: SwapStateHolder) {
private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
Card(
elevation = TangemTheme.dimens.elevation3,
shape = CircleShape,
backgroundColor = TangemTheme.colors.background.plain,
contentColor = TangemTheme.colors.text.primary1,
modifier = Modifier.size(TangemTheme.dimens.size48),
modifier = modifier.size(TangemTheme.dimens.size48),
onClick = state.onChangeCardsClicked,
enabled = !state.updateInProgress,
) {
@ -262,7 +251,6 @@ private fun FeeItem(feeState: FeeState, currency: String) {
@Composable
private fun SwapWarnings(
warnings: List<SwapWarning>,
onPermissionWarningClick: () -> Unit,
) {
Column(
modifier = Modifier
@ -275,7 +263,7 @@ private fun SwapWarnings(
// WarningCard(title = stringResource(id = R.string.), description = stringResource(id = R.string.))
// }
is SwapWarning.PermissionNeeded -> {
ClickableWarningCard(
WarningCard(
title = stringResource(id = R.string.swapping_permission_header),
description = stringResource(
id = R.string.swapping_permission_subheader,
@ -288,7 +276,6 @@ private fun SwapWarnings(
modifier = Modifier.size(TangemTheme.dimens.size20),
)
},
onClick = onPermissionWarningClick,
)
}
is SwapWarning.GenericWarning -> {
@ -311,10 +298,44 @@ private fun SwapWarnings(
}
}
@Composable
private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) {
when {
state.warnings.any { it is SwapWarning.PermissionNeeded } -> {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_give_permission),
enabled = true,
showProgress = state.swapButton.loading,
onClick = onPermissionWarningClick,
)
}
state.warnings.any { it is SwapWarning.InsufficientFunds } -> {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_insufficient_funds),
enabled = false,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
}
else -> {
PrimaryButtonIconRight(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_swap),
icon = painterResource(id = R.drawable.ic_tangem_24),
enabled = state.swapButton.enabled,
showProgress = state.swapButton.loading,
onClick = state.swapButton.onClick,
)
}
}
}
// region preview
private val sendCard = SwapCardData(
type = TransactionCardType.SendCard {},
type = TransactionCardType.SendCard({}) {},
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -24,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -32,7 +34,9 @@ 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.extensions.getActiveIconRes
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.Network
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.TokenBalanceData
import com.tangem.feature.swap.models.TokenToSelect
@ -53,6 +57,8 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit)
placeholderSearchText = stringResource(id = R.string.search_tokens_title),
onSearchChange = state.onSearchEntered,
onSearchDisplayClose = { state.onSearchEntered("") },
subtitle = state.network.name,
icon = painterResource(id = getActiveIconRes(state.network.blockchainId)),
)
},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
@ -69,10 +75,12 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
stickyHeader { Header(title = R.string.swapping_token_list_your_tokens) }
if (state.addedTokens.isNotEmpty()) {
stickyHeader { Header(title = R.string.swapping_token_list_your_tokens) }
}
itemsIndexed(items = state.addedTokens) { index, item ->
TokenItem(token = item, onTokenClick = { state.onTokenSelected(item.id) })
TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) })
if (index != state.addedTokens.lastIndex) {
Divider(
@ -82,10 +90,12 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
}
}
stickyHeader { Header(title = R.string.swapping_token_list_other_tokens) }
if (state.otherTokens.isNotEmpty()) {
stickyHeader { Header(title = R.string.swapping_token_list_other_tokens) }
}
itemsIndexed(items = state.otherTokens) { index, item ->
TokenItem(token = item, onTokenClick = { state.onTokenSelected(item.id) })
TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) })
if (index != state.otherTokens.lastIndex) {
Divider(
color = TangemTheme.colors.stroke.primary,
@ -112,8 +122,9 @@ private fun Header(@StringRes title: Int) {
)
}
@Suppress("LongMethod")
@Composable
private fun TokenItem(token: TokenToSelect, onTokenClick: () -> Unit) {
private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -124,7 +135,10 @@ private fun TokenItem(token: TokenToSelect, onTokenClick: () -> Unit) {
),
verticalAlignment = Alignment.CenterVertically,
) {
TokenIcon(token = token)
TokenIcon(
token = token,
iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null,
)
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
Text(
@ -176,7 +190,13 @@ private fun TokenItem(token: TokenToSelect, onTokenClick: () -> Unit) {
@Suppress("MagicNumber")
@Composable
private fun TokenIcon(token: TokenToSelect) {
private fun TokenIcon(
token: TokenToSelect,
@DrawableRes iconPlaceholder: Int?,
) {
val data = token.iconUrl.ifEmpty {
iconPlaceholder
}
Box(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing12),
@ -192,7 +212,7 @@ private fun TokenIcon(token: TokenToSelect) {
SubcomposeAsyncImage(
modifier = iconModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(token.iconUrl)
.data(data)
.crossfade(true)
.build(),
contentDescription = token.id,
@ -225,6 +245,7 @@ private val token = TokenToSelect(
name = "USDC",
symbol = "USDC",
iconUrl = "",
isNative = false,
addedTokenBalanceData = TokenBalanceData(
amount = "15 000 $",
amountEquivalent = "15 000 " +
@ -241,6 +262,7 @@ private fun TokenScreenPreview() {
otherTokens = listOf(token, token, token),
onSearchEntered = {},
onTokenSelected = {},
network = Network("Ethereum", "ETH"),
),
onBack = {},
)

View file

@ -7,10 +7,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
@ -21,35 +19,19 @@ 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.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -60,7 +42,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.TransactionCardType
import com.valentinilk.shimmer.shimmer
@ -70,10 +53,11 @@ import com.valentinilk.shimmer.shimmer
fun TransactionCard(
type: TransactionCardType,
balance: String,
amount: String?,
amountEquivalent: String?,
tokenIconUrl: String,
tokenCurrency: String,
amount: String?,
amountEquivalent: String?,
modifier: Modifier = Modifier,
@DrawableRes iconPlaceholder: Int? = null,
@DrawableRes networkIconRes: Int? = null,
onChangeTokenClick: (() -> Unit)? = null,
@ -82,13 +66,13 @@ fun TransactionCard(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
modifier = modifier,
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size116),
verticalArrangement = Arrangement.SpaceBetween,
.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
) {
Header(balance = balance, type = type)
@ -155,14 +139,17 @@ private fun Header(
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size24),
)
SpacerWMax(Modifier.defaultMinSize(minWidth = TangemTheme.dimens.spacing16))
SpacerW16()
if (balance.isNotBlank()) {
Text(
text = stringResource(R.string.common_balance, balance),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = MaterialTheme.typography.body2,
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
.padding(top = TangemTheme.dimens.spacing2),
)
} else {
Box(
@ -188,21 +175,21 @@ private fun Content(
Row(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom,
verticalAlignment = Alignment.Top,
) {
Column(
modifier = Modifier
.padding(
end = TangemTheme.dimens.spacing92,
),
verticalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
) {
val sumTextModifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32)
when (type) {
is TransactionCardType.ReceiveCard -> {
if (amount != null) {
@ -211,10 +198,12 @@ private fun Content(
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
modifier = sumTextModifier,
)
} else {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size102, height = TangemTheme.dimens.size24)
.shimmer()
.background(
@ -226,28 +215,27 @@ private fun Content(
}
is TransactionCardType.SendCard -> {
AutoSizeTextField(
modifier = sumTextModifier,
amount = amount ?: "",
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
}
}
Spacer(
modifier = Modifier
.size(TangemTheme.dimens.size4)
.weight(1f),
)
SpacerH8()
if (amountEquivalent != null) {
Text(
text = amountEquivalent,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.body2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
)
} else {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
.shimmer()
.background(
@ -260,71 +248,6 @@ private fun Content(
}
}
@Suppress("MagicNumber")
@Composable
private fun AutoSizeTextField(amount: String, onAmountChange: (String) -> Unit) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
var shrunkFontSize = TangemTheme.typography.h2.fontSize
val calculateIntrinsics = @Composable {
ParagraphIntrinsics(
text = amount,
style = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
shrunkFontSize *= 0.9f
intrinsics = calculateIntrinsics()
}
}
val customTextSelectionColors = TextSelectionColors(
handleColor = Transparent,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
)
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = amount,
onValueChange = onAmountChange,
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal,
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
decorationBox = { innerTextField ->
if (amount.isBlank()) {
Text(
text = "0",
color = TangemTheme.colors.text.disabled,
style = TangemTheme.typography.h2,
)
}
innerTextField()
},
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
fontSize = shrunkFontSize,
),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
)
}
}
}
@Suppress("MagicNumber")
@Composable
fun Token(
@ -452,7 +375,7 @@ private fun Preview_SwapMainCard_InDarkTheme() {
@Composable
private fun TransactionCardPreview() {
TransactionCard(
type = TransactionCardType.SendCard {},
type = TransactionCardType.SendCard({}) {},
amount = "1 000 000",
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -123,7 +123,11 @@ internal class SwapViewModel @Inject constructor(
}
private fun updateTokensState(dataState: FoundTokensState) {
uiState = stateBuilder.addTokensToState(uiState, dataState)
uiState = stateBuilder.addTokensToState(
uiState = uiState,
dataState = dataState,
networkInfo = blockchainInteractor.getBlockchainInfo(currency.networkId),
)
}
private fun startLoadingQuotes(fromToken: Currency, toToken: Currency, amount: String) {
@ -330,6 +334,13 @@ internal class SwapViewModel @Inject constructor(
}
}
@Suppress("UnusedPrivateMember")
private fun onAmountSelected(selected: Boolean) {
if (selected) {
analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked)
}
}
private fun cutAmountWithDecimals(maxDecimals: Int, amount: String): String {
return getValidatedNumberWithFixedDecimals(amount, maxDecimals)
}
@ -377,6 +388,7 @@ internal class SwapViewModel @Inject constructor(
hidePermissionBottomSheet = {
analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked)
},
onAmountSelected = { onAmountSelected(it) },
)
}