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

@ -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()
}