Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-09 14:16:32 +02:00
parent ae9eb162f6
commit 27d1bf4b9e
21 changed files with 291 additions and 264 deletions

View file

@ -32,7 +32,15 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall, RedesignLarge, Contact
Default,
Large,
Medium,
Small,
ExtraSmall,
RedesignedDefault,
RedesignExtraSmall,
ContactLarge,
ContactDefault,
}
/**
@ -132,8 +140,8 @@ fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: M
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
AccountIconSize.RedesignedDefault -> TangemTheme.typography2.headingSemibold28
AccountIconSize.RedesignExtraSmall -> TangemTheme.typography2.captionMedium11
AccountIconSize.RedesignLarge -> TangemTheme.typography3.heading.medium
AccountIconSize.Contact -> TangemTheme.typography3.body.medium
AccountIconSize.ContactLarge -> TangemTheme.typography3.heading.medium
AccountIconSize.ContactDefault -> TangemTheme.typography3.body.medium
}
val textSize by animateFloatAsState(
@ -168,8 +176,8 @@ private fun AccountIconSize.iconSizeInDp(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 8.dp
AccountIconSize.RedesignedDefault -> 20.dp
AccountIconSize.RedesignExtraSmall -> 8.dp
AccountIconSize.RedesignLarge -> 32.dp
AccountIconSize.Contact -> 20.dp
AccountIconSize.ContactLarge -> 32.dp
AccountIconSize.ContactDefault -> 20.dp
}
fun AccountIconSize.toBoxSize(): Dp = when (this) {
@ -180,8 +188,8 @@ fun AccountIconSize.toBoxSize(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 14.dp
AccountIconSize.RedesignedDefault -> 40.dp
AccountIconSize.RedesignExtraSmall -> 16.dp
AccountIconSize.RedesignLarge -> 80.dp
AccountIconSize.Contact -> 40.dp
AccountIconSize.ContactLarge -> 80.dp
AccountIconSize.ContactDefault -> 40.dp
}
private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
@ -192,8 +200,8 @@ private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 4.dp
AccountIconSize.RedesignedDefault -> 12.dp
AccountIconSize.RedesignExtraSmall -> 6.dp
AccountIconSize.RedesignLarge -> 80.dp
AccountIconSize.Contact -> 100.dp
AccountIconSize.ContactLarge -> 80.dp
AccountIconSize.ContactDefault -> 100.dp
}
@Preview(showBackground = true)
@ -236,9 +244,9 @@ private fun Sample() {
AccountIconSize.Small -> AccountIconSize.ExtraSmall
AccountIconSize.ExtraSmall -> AccountIconSize.RedesignedDefault
AccountIconSize.RedesignedDefault -> AccountIconSize.RedesignExtraSmall
AccountIconSize.RedesignExtraSmall -> AccountIconSize.RedesignLarge
AccountIconSize.RedesignLarge -> AccountIconSize.Contact
AccountIconSize.Contact -> AccountIconSize.Default
AccountIconSize.RedesignExtraSmall -> AccountIconSize.ContactLarge
AccountIconSize.ContactLarge -> AccountIconSize.ContactDefault
AccountIconSize.ContactDefault -> AccountIconSize.Default
}
}) { Text("Change") }

View file

@ -36,12 +36,10 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggab
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.*
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import com.tangem.core.ui.utils.WindowInsetsZero
import dev.chrisbanes.haze.rememberHazeState
/**
* Extra bottom inset that scrollable content under a [BasicBottomSheet] should reserve so it
@ -235,46 +233,48 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
}
val bsContent: @Composable ColumnScope.() -> Unit = {
val contentModifier = when (type) {
Default -> Modifier
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
),
)
Modal -> Modifier
.padding(
start = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x2,
bottom = bottomBarHeight,
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
}
Column(
modifier = contentModifier
.background(containerColor)
.heightIn(max = maxHeight)
.testTag(BaseBottomSheetTestTags.CONTAINER),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Box(modifier = Modifier.fillMaxWidth()) {
if (footer != null) {
FooterOverlay(
measuredFooterHeight = footerHeightDp,
onMeasureFooter = { newHeight ->
if (newHeight != footerHeightDp) {
footerHeightDp = newHeight
}
},
footer = { footer(model) },
content = { content(model) },
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
val contentModifier = when (type) {
Default -> Modifier
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
),
)
} else {
content(model)
Modal -> Modifier
.padding(
start = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x2,
bottom = bottomBarHeight,
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
}
Column(
modifier = contentModifier
.background(containerColor)
.heightIn(max = maxHeight)
.testTag(BaseBottomSheetTestTags.CONTAINER),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Box(modifier = Modifier.fillMaxWidth()) {
if (footer != null) {
FooterOverlay(
measuredFooterHeight = footerHeightDp,
onMeasureFooter = { newHeight ->
if (newHeight != footerHeightDp) {
footerHeightDp = newHeight
}
},
footer = { footer(model) },
content = { content(model) },
)
} else {
content(model)
}
}
}
}

View file

@ -78,7 +78,6 @@ internal class AddAddressModel @Inject constructor(
ChosenNetworks(
address = validation.address,
matched = matched,
displayed = displayedNetworks(matched, selected),
selected = selectedNetworks(matched, selected),
)
}
@ -86,7 +85,7 @@ internal class AddAddressModel @Inject constructor(
.stateIn(
modelScope,
SharingStarted.Eagerly,
ChosenNetworks(address = "", matched = emptyList(), displayed = emptyList(), selected = emptyList()),
ChosenNetworks(address = "", matched = emptyList(), selected = emptyList()),
)
/**
@ -161,7 +160,6 @@ internal class AddAddressModel @Inject constructor(
UpdateAddressValidationTransformer(
address = networks.address,
matchedBlockchains = networks.matched,
displayedBlockchains = networks.displayed,
selectedBlockchains = networks.selected,
isMemoInvalid = memoInvalid,
duplicateName = duplicate,
@ -263,12 +261,6 @@ internal class AddAddressModel @Inject constructor(
)
}
/** What the network block shows: all matched networks until the user narrows them down, then the picked subset. */
private fun displayedNetworks(matched: List<Blockchain>, selected: Set<String>?): List<Blockchain> {
if (selected == null) return matched
return matched.filter { it.toNetworkId() in selected }
}
/**
* What is actually selected for saving. A single matched network is auto-selected (there is nothing to choose and
* the selection screen can't be opened); otherwise the user must pick explicitly before saving.
@ -290,7 +282,6 @@ internal class AddAddressModel @Inject constructor(
private data class ChosenNetworks(
val address: String,
val matched: List<Blockchain>,
val displayed: List<Blockchain>,
val selected: List<Blockchain>,
) {
/** The first selected network that supports a memo / destination tag, if any. */

View file

@ -19,10 +19,11 @@ internal class UpdateAddressInputTransformer(
) : Transformer<AddAddressUM> {
override fun transform(prevState: AddAddressUM): AddAddressUM {
val chosenNetworkState = when {
value.isBlank() -> ChosenNetworkStateUM.Hidden
prevState.chosenNetworkStateUM is ChosenNetworkStateUM.Result -> prevState.chosenNetworkStateUM
else -> ChosenNetworkStateUM.Loading
val chosenNetworkState = when (prevState.chosenNetworkStateUM) {
is ChosenNetworkStateUM.Result,
ChosenNetworkStateUM.SelectNetwork,
-> if (value.isBlank()) ChosenNetworkStateUM.Hidden else prevState.chosenNetworkStateUM
else -> if (value.isBlank()) ChosenNetworkStateUM.Hidden else ChosenNetworkStateUM.Loading
}
val memoField = if (value.isBlank()) {
prevState.memoField.copy(isVisible = false, value = "", isError = false)

View file

@ -15,20 +15,19 @@ import kotlinx.collections.immutable.toImmutableList
/**
* Reflects the result of validating an address (and its memo) in the UI.
*
* [matchedBlockchains] are all supported networks the address resolves to. [displayedBlockchains] is what the network
* block shows all matched networks until the user narrows them down on the SelectNetworks screen, then the picked
* subset. [selectedBlockchains] is what is actually chosen for saving (a single match is auto-selected; for several
* matches the user must pick explicitly). While the address is blank or matches nothing the network selector stays
* [ChosenNetworkStateUM.Hidden]; an invalid (non-empty, matching nothing) address surfaces the error in the field label.
* [matchedBlockchains] are all supported networks the address resolves to. [selectedBlockchains] is what is actually
* chosen for saving (a single match is auto-selected; for several matches the user must pick explicitly on the
* SelectNetworks screen). The network block shows only the selected networks while several match but none is picked
* yet it prompts with [ChosenNetworkStateUM.SelectNetwork] rather than showing every matched network. While the address
* is blank or matches nothing the network selector stays [ChosenNetworkStateUM.Hidden]; an invalid (non-empty, matching
* nothing) address surfaces the error in the field label.
*
* The confirm button is enabled only once at least one network is actually selected (and the memo, if any, is valid)
* showing the available networks is not the same as selecting them. The memo field is shown when a selected network
* supports transaction extras; [isMemoInvalid] marks a malformed memo.
* The confirm button is enabled only once at least one network is actually selected (and the memo, if any, is valid).
* The memo field is shown when a selected network supports transaction extras; [isMemoInvalid] marks a malformed memo.
*/
internal class UpdateAddressValidationTransformer(
private val address: String,
private val matchedBlockchains: List<Blockchain>,
private val displayedBlockchains: List<Blockchain>,
private val selectedBlockchains: List<Blockchain>,
private val isMemoInvalid: Boolean,
private val duplicateName: String?,
@ -40,14 +39,14 @@ internal class UpdateAddressValidationTransformer(
val isDuplicate = duplicateName != null
val isError = isInvalidAddress || isDuplicate
val chosenNetworkState = if (hasMatch) {
ChosenNetworkStateUM.Result(
networkUMList = displayedBlockchains.map(ChosenNetworkConverter()::convert).toImmutableList(),
val chosenNetworkState = when {
!hasMatch -> ChosenNetworkStateUM.Hidden
selectedBlockchains.isEmpty() -> ChosenNetworkStateUM.SelectNetwork
else -> ChosenNetworkStateUM.Result(
networkUMList = selectedBlockchains.map(ChosenNetworkConverter()::convert).toImmutableList(),
// A single matched network leaves nothing to choose, so the selection screen is not opened.
isClickable = matchedBlockchains.size > 1,
)
} else {
ChosenNetworkStateUM.Hidden
}
val label = when {

View file

@ -19,9 +19,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
@ -36,19 +35,14 @@ internal fun AddAddressContent(state: AddAddressUM, modifier: Modifier = Modifie
modifier = modifier
.fillMaxSize()
.background(color = TangemTheme.colors3.bg.primary)
.systemBarsPadding(),
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemTopBar(
TangemTopNavigation(
title = resourceReference(R.string.address_book_add_address),
startContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24),
onClick = state.onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onBack = state.onBackClick,
)
BoxWithConstraints(
modifier = Modifier
@ -135,6 +129,7 @@ private fun NetworkSelector(chosenNetworkStateUM: AddAddressUM.ChosenNetworkStat
when (networkState) {
AddAddressUM.ChosenNetworkStateUM.Hidden -> Box(modifier = Modifier.fillMaxWidth())
AddAddressUM.ChosenNetworkStateUM.Loading,
AddAddressUM.ChosenNetworkStateUM.SelectNetwork,
is AddAddressUM.ChosenNetworkStateUM.Result,
-> NetworkBlock(
modifier = Modifier

View file

@ -46,8 +46,8 @@ internal fun NetworkBlock(
chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM,
modifier: Modifier = Modifier,
) {
val isClickable = chosenNetworkStateUM is AddAddressUM.ChosenNetworkStateUM.Result &&
chosenNetworkStateUM.isClickable
val isClickable = chosenNetworkStateUM is AddAddressUM.ChosenNetworkStateUM.SelectNetwork ||
chosenNetworkStateUM is AddAddressUM.ChosenNetworkStateUM.Result && chosenNetworkStateUM.isClickable
TangemRow(
onClick = if (isClickable) onNetworkSelectClick else null,
verticalAlignment = TangemRowVerticalAlignment.Center,
@ -62,6 +62,7 @@ internal fun NetworkBlock(
endSlot = {
when (chosenNetworkStateUM) {
AddAddressUM.ChosenNetworkStateUM.Loading -> TangemLoader(size = TangemLoaderSize.X20)
AddAddressUM.ChosenNetworkStateUM.SelectNetwork -> SelectNetworkTextBlock()
is AddAddressUM.ChosenNetworkStateUM.Result -> NetworkRow(chosenNetworkStateUM = chosenNetworkStateUM)
AddAddressUM.ChosenNetworkStateUM.Hidden -> Unit
}
@ -69,6 +70,18 @@ internal fun NetworkBlock(
)
}
@Composable
private fun SelectNetworkTextBlock() {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.address_book_select_network),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
)
ChevronIcon()
}
}
@Composable
private fun NetworkRow(chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM.Result) {
Row(verticalAlignment = Alignment.CenterVertically) {
@ -120,7 +133,7 @@ private fun OverlappingNetworkIcons(networks: ImmutableList<NetworkUM>) {
modifier = Modifier
.padding(start = NetworkIconStep * index)
.networkIconRing()
.size(24.dp),
.size(20.dp),
)
}
if (remaining > 0) {
@ -129,7 +142,7 @@ private fun OverlappingNetworkIcons(networks: ImmutableList<NetworkUM>) {
.padding(start = NetworkIconStep * visible.size)
.networkIconRing()
.background(color = TangemTheme.colors3.bg.tertiary)
.heightIn(min = 24.dp)
.heightIn(min = 20.dp)
.padding(vertical = 2.dp, horizontal = 4.dp),
contentAlignment = Alignment.Center,
) {
@ -155,7 +168,7 @@ private fun Modifier.networkIconRing(): Modifier = this
private fun ChevronIcon() {
Icon(
modifier = Modifier
.padding(start = 8.dp)
.padding(start = 4.dp)
.size(20.dp),
tint = TangemTheme.colors3.icon.secondary,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_select_18_24),
@ -168,6 +181,11 @@ private fun ChevronIcon() {
private fun Preview_NetworkBlock() {
TangemThemePreviewRedesign {
Column {
NetworkBlock(
onNetworkSelectClick = {},
chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.SelectNetwork,
)
SpacerH12()
NetworkBlock(
onNetworkSelectClick = {},
chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Result(

View file

@ -43,6 +43,12 @@ internal data class AddAddressUM(
/** A non-blank address is being validated against the supported networks. */
data object Loading : ChosenNetworkStateUM
/**
* The address matched several networks but the user hasn't picked any yet the row prompts them to open the
* selection screen instead of showing network icons.
*/
data object SelectNetwork : ChosenNetworkStateUM
/**
* A valid address resolved to [networkUMList] (the currently selected networks). [isClickable] is `false` when
* the address matched only a single network there is nothing to choose, so the network-selection screen is

View file

@ -19,15 +19,14 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_copy_20
import com.tangem.core.ui.res.generated.icons.ic_cross_24
import com.tangem.features.addressbook.addressinfo.ui.state.AddressInfoUM
import com.tangem.features.addressbook.impl.R
@ -42,16 +41,10 @@ internal fun AddressInfoBottomSheet(state: AddressInfoUM, onDismiss: () -> Unit)
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_cross_24),
onClick = onDismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = { TangemButton.Close(onClick = onDismiss) },
)
},
content = {

View file

@ -21,11 +21,11 @@ import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
@ -51,18 +51,13 @@ internal fun AddressSelectorBottomSheet(
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors3.bg.primary,
type = TangemBottomSheetType.Modal,
title = {
TangemTopBar(
TangemTopNavigation(
title = resourceReference(R.string.address_book_choose_address),
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = onDismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onClose = onDismiss,
)
},
content = {

View file

@ -26,7 +26,7 @@ internal fun ContactRow(contact: ContactUM, modifier: Modifier = Modifier) {
AccountIcon(
name = stringReference(contact.name),
icon = contact.icon,
size = AccountIconSize.Contact,
size = AccountIconSize.ContactDefault,
)
},
titleSlot = {

View file

@ -8,11 +8,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -28,16 +30,17 @@ import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.fields.AutoSizeTextField
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@ -53,31 +56,21 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modifier) {
Column(
Box(
modifier = modifier
.fillMaxSize()
.background(color = TangemTheme.colors3.bg.primary)
.imePadding()
.systemBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
.background(color = TangemTheme.colors3.bg.primary),
) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
title = state.title,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = state.onCloseClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
var topBarHeightPx by remember { mutableIntStateOf(0) }
val topBarHeight = with(LocalDensity.current) { topBarHeightPx.toDp() }
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
.fillMaxSize()
.hazeSourceTangem()
.background(color = TangemTheme.colors3.bg.primary)
.imePadding()
.navigationBarsPadding(),
) {
val minContentHeight = maxHeight
Column(
@ -88,7 +81,8 @@ internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modif
Column(
modifier = Modifier
.heightIn(min = minContentHeight)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 16.dp)
.padding(top = topBarHeight + 12.dp, bottom = 12.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
ContactSummary(state = state)
@ -110,6 +104,15 @@ internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modif
}
}
}
TangemTopNavigation(
modifier = Modifier
.align(Alignment.TopCenter)
.onSizeChanged { topBarHeightPx = it.height },
title = state.title,
onClose = state.onCloseClick,
contentAlign = TangemTopNavigation.ContentAlign.Center,
)
}
}
@ -297,7 +300,6 @@ private fun DeleteContactButton(onClick: () -> Unit, modifier: Modifier = Modifi
text = stringResourceSafe(R.string.address_book_delete_contact),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.status.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
@ -333,7 +335,7 @@ private fun ContactSummary(state: EditContactUM) {
AccountIcon(
name = stringReference(avatarName),
icon = state.portfolioIcon,
size = AccountIconSize.RedesignLarge,
size = AccountIconSize.ContactLarge,
)
SpacerH(28.dp)

View file

@ -95,7 +95,10 @@ internal class UpdateAddressBookListContentTransformer(
isActive = false,
onActiveChange = onActiveChange,
onClearClick = onClearQuery,
onCloseClick = { onActiveChange(false) },
onCloseClick = {
onClearQuery()
onActiveChange(false)
},
)
private fun buildChips(matchingWalletIds: List<String>, effectiveSelected: String?) = buildList {

View file

@ -15,14 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
import com.tangem.core.ui.res.generated.icons.ic_sign_plus_20
@Composable
@ -35,17 +34,11 @@ internal fun AddressBookEmptyScreen(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
TangemTopNavigation(
title = resourceReference(R.string.address_book_title),
startContent = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_chevron_left_20),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onBack = onBackClick,
)
NoContactInfo(onAddClick = onAddContactClick)
}

View file

@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.ds2.search.TangemSearch
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
@ -44,23 +44,15 @@ internal fun AddressBookListScreen(
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(modifier = modifier.fillMaxSize()) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
TangemTopNavigation(
title = resourceReference(R.string.address_book_title),
startContent = when (state.contentMode) {
is ContentMode.Default -> {
{
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_chevron_left_20),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
}
}
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onBack = when (state.contentMode) {
is ContentMode.Default -> onBackClick
ContentMode.Select -> null
},
endContent = {
endButton = {
TangemButton(
iconStart = TangemIconUM.Icon(
imageVector = when (state.contentMode) {

View file

@ -11,30 +11,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.shimmers.ProvideTangemShimmer
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
@Composable
internal fun AddressBookListShimmer(onBackClick: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxSize()) {
TangemTopBar(
modifier = Modifier.statusBarsPadding(),
TangemTopNavigation(
title = resourceReference(R.string.address_book_title),
startContent = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_chevron_left_20),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onBack = onBackClick,
)
ProvideTangemShimmer {
Column(

View file

@ -16,7 +16,10 @@ internal class UpdateSelectNetworksInitialStateTransformer(
searchBar = prevState.searchBar.copy(
onQueryChange = onQueryChange,
onActiveChange = onActiveChange,
onCloseClick = { onActiveChange(false) },
onCloseClick = {
onQueryChange("")
onActiveChange(false)
},
onClearClick = { onQueryChange("") },
),
doneButton = prevState.doneButton.copy(onClick = onDoneClick),

View file

@ -15,6 +15,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -28,16 +29,17 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.checkbox.TangemCheckmark
import com.tangem.core.ui.ds2.search.TangemSearch
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_search_24
import com.tangem.features.addressbook.selectnetworks.ui.state.SelectNetworksUM
import com.tangem.features.addressbook.selectnetworks.ui.state.SelectNetworksUM.NetworkItemUM
import kotlinx.collections.immutable.persistentListOf
@ -48,18 +50,13 @@ internal fun SelectNetworksContent(state: SelectNetworksUM, modifier: Modifier =
modifier = modifier
.fillMaxSize()
.background(color = TangemTheme.colors3.bg.primary)
.systemBarsPadding(),
.navigationBarsPadding(),
) {
TangemTopBar(
TangemTopNavigation(
title = resourceReference(R.string.common_choose_network),
startContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24),
onClick = state.onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onBack = state.onBackClick,
)
TangemSearch(
state = state.searchBar,
@ -67,46 +64,89 @@ internal fun SelectNetworksContent(state: SelectNetworksUM, modifier: Modifier =
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
)
val doneButtonVerticalPadding = 12.dp
val doneButtonAreaHeight = 48.dp + doneButtonVerticalPadding * 2
Box(
AnimatedContent(
targetState = state.networks.isNotEmpty(),
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = doneButtonAreaHeight)
.background(
color = TangemTheme.colors3.bg.secondary,
shape = RoundedCornerShape(24.dp),
),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
item {
NetworksHeader(
selectAllButton = state.selectAllButton,
onSelectAllClick = state.onSelectAllClick,
)
}
items(items = state.networks, key = NetworkItemUM::id) { item ->
NetworkRow(item = item)
) { hasNetworks ->
Box(modifier = Modifier.fillMaxSize()) {
if (hasNetworks) {
Content(state = state)
} else {
NothingFoundContent(modifier = Modifier.fillMaxSize())
}
}
TangemButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = doneButtonVerticalPadding)
.imePadding(),
onClick = state.doneButton.onClick,
isEnabled = state.doneButton.isEnabled,
size = TangemButton.Size.X12,
text = state.doneButton.text,
}
}
}
@Composable
private fun BoxScope.Content(state: SelectNetworksUM, modifier: Modifier = Modifier) {
val doneButtonVerticalPadding = 12.dp
val doneButtonAreaHeight = 48.dp + doneButtonVerticalPadding * 2
LazyColumn(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = doneButtonAreaHeight)
.background(
color = TangemTheme.colors3.bg.secondary,
shape = RoundedCornerShape(24.dp),
),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
item {
NetworksHeader(
selectAllButton = state.selectAllButton,
onSelectAllClick = state.onSelectAllClick,
)
}
items(items = state.networks, key = NetworkItemUM::id) { item ->
NetworkRow(item = item)
}
}
TangemButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = doneButtonVerticalPadding)
.imePadding(),
onClick = state.doneButton.onClick,
isEnabled = state.doneButton.isEnabled,
size = TangemButton.Size.X12,
text = state.doneButton.text,
)
}
@Composable
private fun NothingFoundContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.imePadding(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(48.dp)
.background(color = TangemTheme.colors3.bg.opaque.primary, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.ic_search_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.secondary,
modifier = Modifier.size(24.dp),
)
}
Text(
modifier = Modifier.padding(top = 32.dp),
text = stringResourceSafe(R.string.address_book_search_no_results),
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.caption.medium,
)
}
}

View file

@ -241,7 +241,7 @@ internal class AddAddressModelTest {
}
@Test
fun `GIVEN address matching several networks WHEN validated THEN all shown AND clickable`() = runTest {
fun `GIVEN address matching several networks WHEN validated THEN prompts to select network`() = runTest {
// Arrange
every { supportedNetworksMatcher.match(ADDRESS) } returns listOf(Blockchain.Ethereum, Blockchain.BSC)
val model = createModel(testScope = this)
@ -251,11 +251,8 @@ internal class AddAddressModelTest {
model.state.value.onAddressChange(ADDRESS)
advanceUntilIdle()
// Assert — all matched networks are shown by default; the block opens the selection screen to narrow them.
val result = model.state.value.chosenNetworkStateUM as ChosenNetworkStateUM.Result
assertThat(result.networkUMList.map { it.networkName })
.containsExactly(Blockchain.Ethereum.fullName, Blockchain.BSC.fullName)
assertThat(result.isClickable).isTrue()
// Assert — nothing is selected yet, so the block only prompts the user to open the selection screen.
assertThat(model.state.value.chosenNetworkStateUM).isEqualTo(ChosenNetworkStateUM.SelectNetwork)
}
@Test
@ -348,19 +345,19 @@ internal class AddAddressModelTest {
}
@Test
fun `GIVEN resolved networks WHEN address edited THEN keeps result without flashing loading`() = runTest {
// Arrange
fun `GIVEN resolved networks WHEN address edited THEN keeps state without flashing loading`() = runTest {
// Arrange — several networks match, so the resolved state is the "select network" prompt.
every { supportedNetworksMatcher.match(any()) } returns listOf(Blockchain.Ethereum, Blockchain.BSC)
val model = createModel(testScope = this)
model.state.value.onAddressChange(ADDRESS)
advanceUntilIdle()
assertThat(model.state.value.chosenNetworkStateUM).isInstanceOf(ChosenNetworkStateUM.Result::class.java)
assertThat(model.state.value.chosenNetworkStateUM).isEqualTo(ChosenNetworkStateUM.SelectNetwork)
// Act — keep typing; validation is pending again.
model.state.value.onAddressChange(ADDRESS + "00")
// Assert — the resolved networks stay on screen (no spinner), but the button is blocked while validating.
assertThat(model.state.value.chosenNetworkStateUM).isInstanceOf(ChosenNetworkStateUM.Result::class.java)
// Assert — the resolved state stays on screen (no spinner), but the button is blocked while validating.
assertThat(model.state.value.chosenNetworkStateUM).isEqualTo(ChosenNetworkStateUM.SelectNetwork)
assertThat(model.state.value.buttonUM.isEnabled).isFalse()
}

View file

@ -9,15 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorUM
@Composable
@ -27,20 +27,21 @@ internal fun PortfolioSelectorBS(
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = onBack,
scrollableContent = false,
containerColor = TangemTheme.colors.background.tertiary,
containerColor = TangemTheme.colors3.bg.primary,
type = TangemBottomSheetType.Modal,
title = {
TangemModalBottomSheetTitle(
TangemTopNavigation(
title = state.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onBack,
contentAlign = TangemTopNavigation.ContentAlign.Center,
blurBackground = false,
onClose = onDismiss,
)
},
content = {

View file

@ -88,15 +88,15 @@ private fun AddressWithMemoBlock(
val contactIcon = address.contactIcon
Column(modifier = Modifier.weight(1f)) {
Text(
text = contactName ?: address.value,
text = address.value,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.testTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS),
)
val recipient = if (contactName != null) address.value else address.briefBlockchainAddress
if (!recipient.isNullOrBlank()) {
val blockchainAddress = address.briefBlockchainAddress
if (!blockchainAddress.isNullOrBlank()) {
Text(
text = recipient,
text = blockchainAddress,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SendConfirmScreenTestTags.BLOCKCHAIN_ADDRESS),
@ -107,7 +107,7 @@ private fun AddressWithMemoBlock(
AccountIcon(
name = stringReference(contactName),
icon = contactIcon,
size = AccountIconSize.Medium,
size = AccountIconSize.ContactDefault,
modifier = Modifier.testTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS_ICON),
)
} else {