diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt index 94c0d8aaa8..22fcfa94f7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt @@ -3,19 +3,22 @@ package com.tangem.core.ui.ds.button.action import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.button.SecondaryTangemButton @@ -32,6 +35,8 @@ import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +private val ACTION_BUTTONS_SPACING = 10.dp + /** * Action buttons row * @@ -42,42 +47,73 @@ import kotlinx.collections.immutable.persistentListOf */ @Composable fun ActionButtons(buttons: ImmutableList, modifier: Modifier = Modifier) { - Row( - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalAlignment = Alignment.CenterVertically, + val spacingPx = with(LocalDensity.current) { ACTION_BUTTONS_SPACING.roundToPx() } + + Layout( modifier = modifier, - ) { - buttons.forEachIndexed { index, button -> - key(button.text to index) { - val textColor = if (button.isEnabled) { - TangemTheme.colors2.text.neutral.primary - } else { - TangemTheme.colors2.text.status.disabled - } - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens2.x2_5) - .testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) - .semantics { if (!button.isEnabled) disabled() }, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SecondaryTangemButton( - tangemIconUM = button.tangemIconUM, - onClick = button.onClick, - isEnabled = button.isEnabled, - shape = TangemButtonShape.Rounded, - onLongClick = button.onLongClick, - ) - Text( - text = button.text.orEmpty().resolveReference(), - style = TangemTheme.typography2.subheadlineMedium14, - color = textColor, - maxLines = 1, - ) + content = { + buttons.forEachIndexed { index, button -> + key(button.text to index) { + ActionButton(button = button) } } + }, + ) { measurables, constraints -> + if (measurables.isEmpty()) { + return@Layout layout(constraints.minWidth, constraints.minHeight) {} } + val cellWidth = measurables.maxOf { it.maxIntrinsicWidth(constraints.maxHeight) } + val cellConstraints = Constraints( + minWidth = cellWidth, + maxWidth = cellWidth, + minHeight = 0, + maxHeight = constraints.maxHeight, + ) + val placeables = measurables.map { it.measure(cellConstraints) } + + val contentWidth = cellWidth * placeables.size + spacingPx * (placeables.size - 1) + val width = if (constraints.hasBoundedWidth) maxOf(constraints.maxWidth, contentWidth) else contentWidth + val height = placeables.maxOf { it.height } + + layout(width, height) { + var x = (width - contentWidth) / 2 + placeables.forEach { placeable -> + placeable.place(x = x, y = (height - placeable.height) / 2) + x += cellWidth + spacingPx + } + } + } +} + +@Composable +private fun ActionButton(button: TangemButtonUM, modifier: Modifier = Modifier) { + val textColor = if (button.isEnabled) { + TangemTheme.colors2.text.neutral.primary + } else { + TangemTheme.colors2.text.status.disabled + } + Column( + modifier = modifier + .padding(horizontal = TangemTheme.dimens2.x2_5) + .testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) + .semantics { if (!button.isEnabled) disabled() }, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SecondaryTangemButton( + tangemIconUM = button.tangemIconUM, + onClick = button.onClick, + isEnabled = button.isEnabled, + shape = TangemButtonShape.Rounded, + onLongClick = button.onLongClick, + ) + Text( + text = button.text.orEmpty().resolveReference(), + style = TangemTheme.typography2.subheadlineMedium14, + color = textColor, + textAlign = TextAlign.Center, + maxLines = 1, + ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt index 0ab1068bb0..87767f8fd1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt @@ -18,7 +18,7 @@ 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_cloud_24_filled -private const val DEFAULT_DEVICE_ICON_COLOR = 0xFF2C2C2C +private const val DEFAULT_DEVICE_ICON_COLOR = 0xFF595963 private const val DEFAULT_BORDER_COLOR = 0x1A1E1E1E /** diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/button/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/button/TangemButton.kt index 4dce68a7c7..38af7a6b47 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/button/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/button/TangemButton.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -108,7 +108,7 @@ fun TangemButton( enabled = isEnabled, color = backgroundColor, border = resolveBorder(isFocused = isFocused, colorTokens = colorTokens, contentAlpha = contentAlpha), - shape = RoundedCornerShape(999.dp), + shape = CircleShape, interactionSource = interactionSource, isMaterial = variant == TangemButton.Variant.Material, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/search/TangemSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/search/TangemSearch.kt index 9dad055473..bd6ed68653 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/search/TangemSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/search/TangemSearch.kt @@ -250,7 +250,7 @@ private fun CloseButton(onClick: () -> Unit) { @Composable private fun Preview(@PreviewParameter(TangemSearchStateProvider::class) state: TangemSearch.State) { TangemThemePreviewRedesign { - Box(modifier = Modifier.background(TangemTheme.colors3.bg.secondary)) { + Box(modifier = Modifier.background(TangemTheme.colors3.bg.tertiary)) { TangemSearch( state = state, modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/surface/TangemSurface.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/surface/TangemSurface.kt index c62c54918d..2b4f731dac 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/surface/TangemSurface.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/surface/TangemSurface.kt @@ -12,8 +12,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -113,7 +117,7 @@ fun TangemSurface( @Composable private fun Modifier.materialShadow(shape: Shape): Modifier = softLayerShadow( radius = 40.dp, - color = Color.Black.copy(alpha = 0.10f), + color = Color.Black.copy(alpha = 0.12f), shape = shape, spread = 0.dp, offset = DpOffset(x = 0.dp, y = 8.dp), @@ -141,19 +145,22 @@ private fun Modifier.materialBorder(shape: Shape): Modifier = border( @Composable private fun Modifier.materialFill(): Modifier { val isBlurEnabled = LocalHazeState.current.blurEnabled + val material = TangemTheme.colors3.material val hazed = hazeEffectTangem( style = HazeStyle( - backgroundColor = TangemTheme.colors3.material.fill.blur, + backgroundColor = Color.Transparent, blurRadius = 32.dp, - tints = emptyList(), + tints = listOf( + HazeTint(material.fill.blur), + ), ), ) { fallbackTint = HazeTint(Color.Transparent) } return hazed.conditionalCompose(!isBlurEnabled) { // Paint the opaque fill first, then layer the translucent tint on top so both are visible. - background(TangemTheme.colors3.material.fill.solid) - .background(TangemTheme.colors3.material.tint.solid) + background(material.fill.solid) + .background(material.tint.solid) } } @@ -162,13 +169,22 @@ private fun Modifier.materialFill(): Modifier { @ReadOnlyComposable private fun materialBorderBrush(): Brush { val border = TangemTheme.colors3.material.border - return Brush.linearGradient( - 0f to border.start, - 0.5f to border.mid, - 1f to border.end, - start = Offset.Zero, - end = Offset.Infinite, - ) + val startColor = border.start + val midColor = Color.Transparent + val endColor = border.end + return object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val w = size.width + val h = size.height + val k = 2f * w * h / (w * w + h * h) + return LinearGradientShader( + from = Offset.Zero, + to = Offset(x = k * h, y = k * w), + colors = listOf(startColor, midColor, midColor, endColor), + colorStops = listOf(0f, 0.40f, 0.60f, 1f), + ) + } + } } // endregion diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt index 9473717202..97b85281f2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/topnavigation/TangemTopNavigation.kt @@ -47,6 +47,9 @@ private enum class SlotId { Start, Content, Group, End } * @param contentAlign How [contentColumn] is aligned horizontally within the bar. * @param windowInsets Top inset applied above the row. Pass `WindowInsets(0)` inside a bottom * sheet / modal. + * @param contentPadding Inner padding applied to the row, inside [windowInsets]. Defaults to + * [TangemTopNavigation.DefaultContentPadding] (top 8, bottom 16, horizontal 16). Override a single + * edge via [com.tangem.core.ui.extensions.copy], e.g. `DefaultContentPadding.copy(top = 16.dp)`. * @param blurBackground Whether the fade behind the row should blur the content below. * @param startButton Leading slot. Typically a back button (see [TangemButton.Back]). * @param endButtonsGroup Optional pill-grouped secondary actions placed just before [endButton]. @@ -59,6 +62,7 @@ fun TangemTopNavigation( modifier: Modifier = Modifier, contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start, windowInsets: WindowInsets = WindowInsets.statusBars, + contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding, blurBackground: Boolean = true, startButton: (@Composable () -> Unit)? = null, endButtonsGroup: (@Composable RowScope.() -> Unit)? = null, @@ -89,12 +93,7 @@ fun TangemTopNavigation( modifier = Modifier .fillMaxWidth() .windowInsetsPadding(windowInsets) - .padding( - top = 8.dp, - bottom = 16.dp, - start = 16.dp, - end = 16.dp, - ), + .padding(contentPadding), content = { val displayedStart = rememberLastNonNull(startButton) Box(modifier = Modifier.layoutId(SlotId.Start)) { @@ -208,6 +207,7 @@ fun TangemTopNavigation( subtitle: TextReference? = null, contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start, windowInsets: WindowInsets = WindowInsets.statusBars, + contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding, blurBackground: Boolean = true, onBack: (() -> Unit)? = null, endButtonsGroup: (@Composable RowScope.() -> Unit)? = null, @@ -217,6 +217,7 @@ fun TangemTopNavigation( modifier = modifier, contentAlign = contentAlign, windowInsets = windowInsets, + contentPadding = contentPadding, blurBackground = blurBackground, startButton = onBack?.let { { TangemButton.Back(onClick = it) } }, endButtonsGroup = endButtonsGroup, @@ -233,6 +234,7 @@ fun TangemTopNavigation( subtitle: TextReference? = null, contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start, windowInsets: WindowInsets = WindowInsets.statusBars, + contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding, blurBackground: Boolean = true, endButtonsGroup: (@Composable RowScope.() -> Unit)? = null, onClose: (() -> Unit)? = null, @@ -242,6 +244,7 @@ fun TangemTopNavigation( modifier = modifier, contentAlign = contentAlign, windowInsets = windowInsets, + contentPadding = contentPadding, blurBackground = blurBackground, startButton = startButton, endButtonsGroup = endButtonsGroup, @@ -258,6 +261,7 @@ fun TangemTopNavigation( subtitle: TextReference? = null, contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start, windowInsets: WindowInsets = WindowInsets.statusBars, + contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding, blurBackground: Boolean = true, onBack: (() -> Unit)? = null, endButton: @Composable () -> Unit, @@ -266,6 +270,7 @@ fun TangemTopNavigation( modifier = modifier, contentAlign = contentAlign, windowInsets = windowInsets, + contentPadding = contentPadding, blurBackground = blurBackground, startButton = onBack?.let { { TangemButton.Back(onClick = it) } }, endButton = endButton, @@ -308,7 +313,7 @@ private fun ColumnScope.TitleSubtitle(title: TextReference, subtitle: TextRefere ) { displayedSubtitle?.let { text -> Column { - Spacer(Modifier.height(2.dp)) + Spacer(Modifier.height(4.dp)) TangemNavigationText(text = text, role = TangemNavigationText.Role.Subtitle) } } @@ -317,6 +322,15 @@ private fun ColumnScope.TitleSubtitle(title: TextReference, subtitle: TextRefere object TangemTopNavigation { + /** Default inner padding of the row: top 8, bottom 16, horizontal 16. */ + @Suppress("MagicNumber") + val DefaultContentPadding: PaddingValues = PaddingValues( + top = 8.dp, + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ) + /** Horizontal alignment of the center content slot. */ enum class ContentAlign { Start, Center diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/PaddingValuesExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/PaddingValuesExt.kt new file mode 100644 index 0000000000..c6936d90f2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/PaddingValuesExt.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp + +/** + * Returns a copy of this [PaddingValues] with the given edges overridden, leaving the rest unchanged. + */ +@Composable +fun PaddingValues.copy(start: Dp? = null, top: Dp? = null, end: Dp? = null, bottom: Dp? = null): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + return PaddingValues( + start = start ?: calculateStartPadding(layoutDirection), + top = top ?: calculateTopPadding(), + end = end ?: calculateEndPadding(layoutDirection), + bottom = bottom ?: calculateBottomPadding(), + ) +} \ No newline at end of file diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/model/ChooseTokenPortfolioFullBlockUM.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/model/ChooseTokenPortfolioFullBlockUM.kt index 97a3274e8b..d5d9e35c13 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/model/ChooseTokenPortfolioFullBlockUM.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/model/ChooseTokenPortfolioFullBlockUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.commonfeatures.api.choosetoken.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -22,6 +23,7 @@ data class WalletTabUM( val count: TextReference?, val isSelected: Boolean, val onClick: () -> Unit, + val deviceIcon: DeviceIconUM, ) @Immutable diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt index 1b7b276b63..4050295fce 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt @@ -1,5 +1,6 @@ package com.tangem.features.commonfeatures.impl.choosetoken.model +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet @@ -7,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery @@ -30,6 +32,8 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( private val settingContextUseCase: SettingContextUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getWalletIconUseCase: GetWalletIconUseCase, + private val walletIconUMConverter: WalletIconUMConverter, private val dispatchers: CoroutineDispatcherProvider, private val selectedWalletUseCase: GetSelectedWalletUseCase, @Assisted private val modelScope: CoroutineScope, @@ -87,6 +91,7 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( onClick = { selectWalletTab(walletId) }, isSelected = selectedWalletId == walletId, count = searchResultCount, + deviceIcon = walletIconUMConverter.convert(getWalletIconUseCase(wallet)), ) } val walletListUM = if (walletsUM.size != 1) { diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt index 18fbcd8716..e455ef9b71 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt @@ -12,6 +12,7 @@ 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.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag @@ -38,6 +39,8 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @@ -262,6 +265,13 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) { style = TangemTheme.typography2.bodySemibold16, ) + Spacer(modifier = Modifier.width(4.dp)) + + TangemDeviceIcon( + state = state.deviceIcon, + modifier = Modifier.size(20.dp), + ) + val count = state.count if (count != null) { Spacer(modifier = Modifier.width(8.dp)) @@ -464,24 +474,31 @@ private val wallets isSelected = true, onClick = {}, count = null, + deviceIcon = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), ), WalletTabUM( text = TextReference.Str(value = "Wallet 1"), isSelected = true, onClick = {}, - count = stringReference("3"), + count = null, + deviceIcon = DeviceIconUM.Mobile, ), WalletTabUM( text = TextReference.Str(value = "Wallet 2"), isSelected = false, onClick = {}, - count = stringReference("333"), + count = stringReference("3"), + deviceIcon = DeviceIconUM.Ring(), ), WalletTabUM( text = TextReference.Str(value = "Wallet 3"), isSelected = false, onClick = {}, count = null, + deviceIcon = DeviceIconUM.Mobile, ), ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 209108a216..3884207e3c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -55,6 +55,7 @@ sealed interface SwapState { val isFeeCoverage: Boolean, val sendingAmount: BigDecimal, val tronFeeNotificationShowCount: Int, + val isAmountSubtractAvailable: Boolean, val isSendingAmountLoading: Boolean = false, val currencyCheck: CryptoCurrencyCheck? = null, val validationResult: Throwable? = null, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt index ac06572551..e6def26c9f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt @@ -110,9 +110,14 @@ class SwapTransferInteractorImpl @Inject constructor( fee = warningsFee, feeCurrencyBalanceAfterTransaction = null, ) + val isAmountSubtractAvailable = isAmountSubtractAvailable( + userWalletId = userWallet.walletId, + currency = fromTokenInfo.swapCurrencyStatus.currency, + fee = fee, + ) val coverageState = getCoverageState( fromTokenInfo = fromTokenInfo, - userWallet = userWallet, + isAmountSubtractAvailable = isAmountSubtractAvailable, fee = fee, currencyCheck = currencyCheck, ) @@ -137,6 +142,7 @@ class SwapTransferInteractorImpl @Inject constructor( isFeeCoverage = coverageState.isFeeCoverage, sendingAmount = coverageState.sendingAmount, tronFeeNotificationShowCount = tronFeeNotificationShowCount, + isAmountSubtractAvailable = isAmountSubtractAvailable, isSendingAmountLoading = coverageState.isSendingAmountLoading, currencyCheck = currencyCheck, ) @@ -156,18 +162,13 @@ class SwapTransferInteractorImpl @Inject constructor( ).getOrNull() } - private suspend fun getCoverageState( + private fun getCoverageState( fromTokenInfo: TokenSwapInfo, - userWallet: UserWallet, + isAmountSubtractAvailable: Boolean, fee: Fee?, currencyCheck: CryptoCurrencyCheck, ): CoverageState { val swapCurrencyStatus = fromTokenInfo.swapCurrencyStatus - val isAmountSubtractAvailable = isAmountSubtractAvailable( - userWalletId = userWallet.walletId, - currency = swapCurrencyStatus.currency, - fee = fee, - ) val balance = swapCurrencyStatus.status.value.amount ?: BigDecimal.ZERO val reduceAmountBy = currencyCheck.existentialDeposit.orZero() val amount = fromTokenInfo.tokenAmount diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt index b140e07270..6ff5e5270d 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt @@ -187,6 +187,7 @@ internal class SwapTransferInteractorImplTest { isFeeCoverage = false, sendingAmount = expectedAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, currencyCheck = currencyCheck, ) assertThat(result).isEqualTo(expected) @@ -259,6 +260,7 @@ internal class SwapTransferInteractorImplTest { isFeeCoverage = false, sendingAmount = expectedAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, currencyCheck = currencyCheck, ) assertThat(result).isEqualTo(expected) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index bd36406530..b4f49fcafb 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -824,8 +824,7 @@ internal class SwapModel @Inject constructor( transferState = swapState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = feePaidCryptoCurrency, - fee = selectedFee, - feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, + feeSelectorUM = feeSelectorRepository.state.value, ) when { uiState.successState != null -> Unit @@ -873,7 +872,7 @@ internal class SwapModel @Inject constructor( feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus ?: dataState.feePaidCryptoCurrency, fee = fee, isTangemPayWithdrawal = isTangemPayWithdrawal(), - feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, + feeSelectorUM = feeSelectorRepository.state.value, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt index 9508fd1e4b..e863c9ef7b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt @@ -1,8 +1,8 @@ package com.tangem.feature.swap.ui.transfer -import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification @@ -18,6 +18,8 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM +import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos @@ -33,22 +35,30 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { @Suppress("LongParameterList") fun getNotifications( transferState: SwapState.Transfer, + feeSelectorUM: FeeSelectorUM?, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, actions: UiActions, - getFeeError: GetFeeError?, ): ImmutableList { + // The fee selector exposes a single sealed state; narrow it here so call sites pass the raw + // FeeSelectorUM and this factory owns the Content/Error/Loading discrimination. + val feeContent = feeSelectorUM + val getFeeError = (feeSelectorUM as? FeeSelectorUM.Error)?.error return buildList { maybeAddRentExemptionError(transferState) maybeAddDomainWarnings( state = transferState, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, + feeSelectorUM = feeContent, onReduceByAmount = actions.onReduceByAmount, onReduceToAmount = actions.onReduceToAmount, ) maybeAddNeedReserveToCreateAccountWarning(transferState) - maybeAddExceedsBalanceNotification(transferState, onBuyClick = actions.openTokenDetailsScreen) + maybeAddExceedsBalanceNotifications( + transferState = transferState, + feeSelectorUM = feeContent, + onBuyClick = actions.openTokenDetailsScreen, + ) + maybeAddTooHighOrTooLowNotification(feeContent) addTronNetworkFeesNotification( cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, transferState = transferState, @@ -71,13 +81,14 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { private fun MutableList.maybeAddDomainWarnings( state: SwapState.Transfer, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, + feeSelectorUM: FeeSelectorUM?, onReduceByAmount: (SwapAmount, BigDecimal) -> Unit, onReduceToAmount: (SwapAmount) -> Unit, ) { val swapCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus val amount = state.fromTokenInfo.tokenAmount val balance = swapCurrencyStatus.status.value.amount ?: BigDecimal.ZERO + val fee = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee val feeValue = fee?.amount?.value.orZero() val isCardano = BlockchainUtils.isCardano(swapCurrencyStatus.currency.network.rawId) addExistentialWarningNotification( @@ -191,8 +202,9 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { } } - private fun MutableList.maybeAddExceedsBalanceNotification( + private fun MutableList.maybeAddExceedsBalanceNotifications( transferState: SwapState.Transfer, + feeSelectorUM: FeeSelectorUM?, onBuyClick: (CryptoCurrency) -> Unit, ) { val cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status @@ -206,6 +218,27 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { onAnalyticsEvent = {}, onResetAnalyticsEvent = {}, ) + val feeAmount = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee?.amount?.value + if (feeAmount != null) { + addExceedBalanceNotification( + feeAmount = feeAmount, + sendingAmount = transferState.sendingAmount, + isSubtractionAvailable = transferState.isAmountSubtractAvailable, + cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, + ) + } + } + + @Suppress("CanBeNonNullable") + private fun MutableList.maybeAddTooHighOrTooLowNotification(feeSelectorUM: FeeSelectorUM?) { + val content = feeSelectorUM as? FeeSelectorUM.Content ?: return + val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM = content) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM = content)) { + add(NotificationUM.Warning.FeeTooLow) + } } private fun MutableList.addTronNetworkFeesNotification( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt index b7f14dbffd..e593d981d1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt @@ -59,8 +59,7 @@ internal class SwapTransferStateBuilder @Inject constructor( transferState: SwapState.Transfer, uiStateHolder: SwapStateHolder, feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, - feeError: FeeSelectorUM.Error?, + feeSelectorUM: FeeSelectorUM?, ): SwapStateHolder { val fromTokenSwapInfo = transferState.fromTokenInfo val isInsufficientBalance = transferState.isInsufficientBalance @@ -68,10 +67,9 @@ internal class SwapTransferStateBuilder @Inject constructor( val prevAmountField = prevSendCard?.amountField val notifications = notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = feeSelectorUM, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, - fee = fee, actions = actions, - getFeeError = feeError?.error, ) return uiStateHolder.copy( sendCardData = createSendSwapCardState( @@ -344,14 +342,13 @@ internal class SwapTransferStateBuilder @Inject constructor( feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, fee: Fee?, isTangemPayWithdrawal: Boolean, - feeError: FeeSelectorUM.Error?, + feeSelectorUM: FeeSelectorUM?, ): SwapStateHolder { val notifications = notificationsFactory.getNotifications( transferState = transferState, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, - fee = fee, + feeSelectorUM = feeSelectorUM, actions = actions, - getFeeError = feeError?.error, ) return uiStateHolder.copy( notifications = notifications, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt index f415d9db37..0c7767d828 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui.transfer import com.google.common.truth.Truth.assertThat import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account @@ -18,8 +19,12 @@ import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM +import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem +import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM import io.mockk.every import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -43,10 +48,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result).isEmpty() @@ -65,10 +69,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -85,16 +88,13 @@ internal class SwapTransferNotificationsFactoryTest { ), currencyCheck = buildCurrencyCheck(existentialDeposit = BigDecimal("0.5")), ) - val fee: Fee = mockk(relaxed = true) { - every { amount.value } returns BigDecimal("0.4") - } + val feeSelectorUM = contentWithFee(feeValue = BigDecimal("0.4")) val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = feeSelectorUM, feeCryptoCurrencyStatus = null, - fee = fee, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -113,10 +113,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -131,10 +130,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -154,10 +152,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -176,10 +173,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) val reserve = result.filterIsInstance() @@ -199,10 +195,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -220,10 +215,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -242,10 +236,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -264,10 +257,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() @@ -285,10 +277,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() @@ -302,10 +293,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.UnknownError), feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = null, actions = actions, - getFeeError = GetFeeError.UnknownError, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -319,10 +309,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.BlockchainErrors.TronActivationError), feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = null, actions = actions, - getFeeError = GetFeeError.BlockchainErrors.TronActivationError, ) val notifications = result.filterIsInstance() @@ -337,10 +326,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.UnknownError), feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = GetFeeError.UnknownError, ) assertThat(result.filterIsInstance()).isEmpty() @@ -353,15 +341,94 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = buildCoinStatus().status, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() } + @Test + fun `GIVEN custom fee below network minimum WHEN getNotifications THEN FeeTooLow is added`() = runTest { + val transferState = buildTransferState() + val feeSelectorUM = contentWithCustomFeeBelowMinimum( + customFeeValue = "0.0001", + minimumFeeValue = BigDecimal("0.001"), + ) + + val result = sut.getNotifications( + transferState = transferState, + feeSelectorUM = feeSelectorUM, + feeCryptoCurrencyStatus = null, + actions = actions, + ) + + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN custom fee at network minimum WHEN getNotifications THEN no FeeTooLow`() = runTest { + val transferState = buildTransferState() + val feeSelectorUM = contentWithCustomFeeBelowMinimum( + customFeeValue = "0.001", + minimumFeeValue = BigDecimal("0.001"), + ) + + val result = sut.getNotifications( + transferState = transferState, + feeSelectorUM = feeSelectorUM, + feeCryptoCurrencyStatus = null, + actions = actions, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + + /** + * Builds a [FeeSelectorUM.Content] with a Custom fee whose [customFeeValue] is below the choosable + * [minimumFeeValue], so + * [com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow] + * reports the fee as too low. The choosable `priority` is left unstubbed (null) so the sibling + * `checkIfCustomFeeTooHigh` short-circuits and does not add a spurious TooHigh notification. + */ + private fun contentWithCustomFeeBelowMinimum( + customFeeValue: String, + minimumFeeValue: BigDecimal, + decimals: Int = 8, + ): FeeSelectorUM.Content { + val customField: CustomFeeFieldUM = mockk(relaxed = true) { + every { value } returns customFeeValue + every { this@mockk.decimals } returns decimals + } + val customFeeItem: FeeItem.Custom = mockk(relaxed = true) { + every { customValues } returns persistentListOf(customField) + } + val choosableFees: TransactionFee.Choosable = mockk(relaxed = true) { + every { minimum.amount.value } returns minimumFeeValue + } + return mockk(relaxed = true) { + every { selectedFeeItem } returns customFeeItem + every { fees } returns choosableFees + } + } + + /** + * Builds a [FeeSelectorUM.Content] whose selected fee carries [feeValue]. A non-Custom fee item is used so + * [com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh] + * short-circuits and does not add a spurious TooHigh notification. + */ + private fun contentWithFee(feeValue: BigDecimal): FeeSelectorUM.Content { + val fee: Fee = mockk(relaxed = true) { + every { amount.value } returns feeValue + } + return mockk(relaxed = true) { + every { selectedFeeItem } returns FeeItem.Market(fee = fee) + } + } + + private fun errorSelector(error: GetFeeError): FeeSelectorUM.Error = FeeSelectorUM.Error(error = error) + @Suppress("LongParameterList") private fun buildTransferState( fromTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()), @@ -373,6 +440,7 @@ internal class SwapTransferNotificationsFactoryTest { isFeeCoverage: Boolean = false, sendingAmount: BigDecimal = fromTokenInfo.tokenAmount.value, tronFeeNotificationShowCount: Int = 0, + isAmountSubtractAvailable: Boolean = false, ): SwapState.Transfer = SwapState.Transfer( userWallet = coldWallet, fromTokenInfo = fromTokenInfo, @@ -385,6 +453,7 @@ internal class SwapTransferNotificationsFactoryTest { isFeeCoverage = isFeeCoverage, sendingAmount = sendingAmount, tronFeeNotificationShowCount = tronFeeNotificationShowCount, + isAmountSubtractAvailable = isAmountSubtractAvailable, currencyCheck = currencyCheck, validationResult = validationResult, minAdaValue = minAdaValue, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt index 64861e80a5..9b0c42f5dd 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt @@ -39,12 +39,14 @@ import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R +import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import java.math.BigDecimal @@ -57,10 +59,9 @@ internal class SwapTransferStateBuilderTest { coEvery { getNotifications( transferState = any(), + feeSelectorUM = any(), feeCryptoCurrencyStatus = any(), - fee = any(), actions = any(), - getFeeError = any(), ) } returns persistentListOf() } @@ -72,6 +73,21 @@ internal class SwapTransferStateBuilderTest { isFeeApproximateUseCase = isFeeApproximateUseCase, ) + // PER_CLASS reuses the notificationsFactory mock across tests, so clear its recorded calls (and re-stub) + // before each test to keep coVerify(exactly = 1) scoped to the current test. + @BeforeEach + fun resetMocks() { + clearMocks(notificationsFactory) + coEvery { + notificationsFactory.getNotifications( + transferState = any(), + feeSelectorUM = any(), + feeCryptoCurrencyStatus = any(), + actions = any(), + ) + } returns persistentListOf() + } + private val userWalletId = UserWalletId(stringValue = "deadbeef") private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { every { walletId } returns userWalletId @@ -123,8 +139,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio @@ -154,10 +169,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -177,8 +191,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -197,10 +210,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -221,8 +233,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -241,10 +252,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -265,8 +275,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio @@ -291,10 +300,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -338,10 +346,9 @@ internal class SwapTransferStateBuilderTest { coEvery { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = fee, actions = any(), - getFeeError = any(), ) } returns persistentListOf() @@ -353,7 +360,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -362,10 +369,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = fee, actions = any(), - getFeeError = any(), ) } } @@ -387,8 +393,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = mockk(relaxed = true), - feeError = null, + feeSelectorUM = null, ) val sendCard = result.sendCardData as SwapCardState.SwapCardData @@ -422,8 +427,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -450,8 +454,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -482,7 +485,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = mockk(relaxed = true), isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -517,7 +520,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isInstanceOf(TextReference.Combined::class.java) @@ -576,7 +579,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isEqualTo( @@ -622,7 +625,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isEqualTo( @@ -753,7 +756,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = true, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -789,7 +792,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isFalse() @@ -939,6 +942,7 @@ internal class SwapTransferStateBuilderTest { isFeeCoverage = isFeeCoverage, sendingAmount = toAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, isSendingAmountLoading = isSendingAmountLoading, ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt index e7da252c44..29433dc21d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt @@ -262,8 +262,6 @@ private fun SubtitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifi text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_error_subtitle), style = TangemTheme.typography3.body.medium, color = TangemTheme.colors3.text.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, ) } is TangemPayDailyLimitBlockState.Content -> { @@ -272,8 +270,6 @@ private fun SubtitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifi text = state.limit, style = TangemTheme.typography3.body.medium, color = TangemTheme.colors3.text.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, ) } TangemPayDailyLimitBlockState.Loading -> { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/button/TangemButtonStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/button/TangemButtonStory.kt index 7eccf7507b..5aa6c9808c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/button/TangemButtonStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/button/TangemButtonStory.kt @@ -137,7 +137,7 @@ private fun ComponentPreview(state: TangemButtonStory) { background = state.background, modifier = Modifier .matchParentSize() - .hazeSourceTangem(zIndex = 0f), + .hazeSourceTangem(zIndex = -1f), ) Box( contentAlignment = Alignment.Center, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/search/TangemSearchStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/search/TangemSearchStory.kt index 2acb3acec8..c34276949a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/search/TangemSearchStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/search/TangemSearchStory.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.ds2.search.TangemSearch import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -76,7 +77,12 @@ private fun ComponentPreview(state: TangemSearchStory) { .padding(horizontal = 16.dp) .clip(RoundedCornerShape(16.dp)), ) { - PreviewBackground(background = state.background, modifier = Modifier.matchParentSize()) + PreviewBackground( + background = state.background, + modifier = Modifier + .matchParentSize() + .hazeSourceTangem(), + ) TangemSearch( state = TangemSearch.State( placeholderText = stringReference(state.placeholder.text), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt index b327fcdd6a..d4d0a17a0f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt @@ -10,10 +10,12 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.managetokens.model.AddAndManageModel import com.tangem.feature.wallet.child.managetokens.ui.AddAndManageBottomSheetContent +import com.tangem.feature.wallet.child.managetokens.ui.AddAndManageBottomSheetContentLegacy import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import kotlinx.serialization.builtins.serializer @@ -51,12 +53,21 @@ internal class AddAndManageBottomSheetComponent( val portfolioSelectorSlot by portfolioSelectorSlot.subscribeAsState() val state by model.state.collectAsStateWithLifecycle() - AddAndManageBottomSheetContent( - onAddTokensClick = model::onAddTokensClick, - shouldShowOrganizeButton = state.shouldShowOrganize, - onOrganizeTokensClick = model::onOrganizeTokensClick, - onDismiss = ::dismiss, - ) + if (LocalRedesignEnabled.current) { + AddAndManageBottomSheetContent( + onAddTokensClick = model::onAddTokensClick, + shouldShowOrganizeButton = state.shouldShowOrganize, + onOrganizeTokensClick = model::onOrganizeTokensClick, + onDismiss = ::dismiss, + ) + } else { + AddAndManageBottomSheetContentLegacy( + onAddTokensClick = model::onAddTokensClick, + shouldShowOrganizeButton = state.shouldShowOrganize, + onOrganizeTokensClick = model::onOrganizeTokensClick, + onDismiss = ::dismiss, + ) + } portfolioSelectorSlot.child?.instance?.BottomSheet() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt index e41627a9ec..b6a35b5b0a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* 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 @@ -12,21 +13,25 @@ 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.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.res.R as ResR -import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +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.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType +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.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview +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_right_24 +import com.tangem.feature.wallet.impl.R @Composable internal fun AddAndManageBottomSheetContent( @@ -35,24 +40,31 @@ internal fun AddAndManageBottomSheetContent( onOrganizeTokensClick: () -> Unit, onDismiss: () -> Unit, ) { - val config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onDismiss, - content = AddAndManageBottomSheetConfigContent, - ) - - TangemModalBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, + TangemBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + type = TangemBottomSheetType.Modal, + containerColor = TangemTheme.colors2.surface.level2, title = { - TangemModalBottomSheetTitle( - title = resourceReference(ResR.string.main_add_and_manage_tokens), - endIconRes = R.drawable.ic_close_24, - onEndClick = onDismiss, + TangemTopBar( + title = resourceReference(R.string.main_add_and_manage_tokens), + type = TangemTopBarType.BottomSheet, + endContent = { + TangemButton( + iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24), + onClick = onDismiss, + size = TangemButton.Size.X11, + variant = TangemButton.Variant.Material, + ) + }, ) }, content = { AddAndManageContent( + modifier = Modifier.padding(bottom = 16.dp), onAddTokensClick = onAddTokensClick, shouldShowOrganizeButton = shouldShowOrganizeButton, onOrganizeTokensClick = onOrganizeTokensClick, @@ -66,38 +78,29 @@ private fun AddAndManageContent( onAddTokensClick: () -> Unit, shouldShowOrganizeButton: Boolean, onOrganizeTokensClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), + modifier = modifier + .fillMaxWidth() + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { AddAndManageRow( iconRes = R.drawable.ic_plus_24, - title = ResR.string.add_and_manage_sheet_manage_title, - subtitle = ResR.string.add_and_manage_sheet_manage_subtitle, + title = R.string.add_and_manage_sheet_manage_title, + subtitle = R.string.add_and_manage_sheet_manage_subtitle, onClick = onAddTokensClick, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = 0, - lastIndex = if (shouldShowOrganizeButton) 1 else 0, - addDefaultPadding = false, - backgroundColor = TangemTheme.colors.background.action, - ), ) if (shouldShowOrganizeButton) { AddAndManageRow( iconRes = R.drawable.ic_filter_default_24, - title = ResR.string.add_and_manage_sheet_organize_title, - subtitle = ResR.string.add_and_manage_sheet_organize_subtitle, + title = R.string.add_and_manage_sheet_organize_title, + subtitle = R.string.add_and_manage_sheet_organize_subtitle, onClick = onOrganizeTokensClick, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 1, - addDefaultPadding = false, - backgroundColor = TangemTheme.colors.background.action, - ), ) } } @@ -114,50 +117,57 @@ private fun AddAndManageRow( Row( modifier = modifier .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 15.dp), + .background(TangemTheme.colors2.surface.level3) + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier - .size(36.dp) + .size(40.dp) .clip(CircleShape) - .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)), + .background(TangemTheme.colors2.graphic.status.accent.copy(alpha = 0.1f)), ) { Icon( - modifier = Modifier.size(18.dp), - painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), - tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(id = iconRes), + tint = TangemTheme.colors2.markers.iconBlue, contentDescription = null, ) } + SpacerW(12.dp) Column( + modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = stringResourceSafe(id = title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, ) Text( text = stringResourceSafe(id = subtitle), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, ) } + SpacerW(8.dp) + Icon( + imageVector = Icons.ic_chevron_right_24, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + contentDescription = null, + ) } } -private object AddAndManageBottomSheetConfigContent : TangemBottomSheetConfigContent - // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun AddAndManageBottomSheetContent_Preview() { - TangemThemePreview { + TangemThemePreviewRedesign { AddAndManageContent( onAddTokensClick = {}, shouldShowOrganizeButton = true, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContentLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContentLegacy.kt new file mode 100644 index 0000000000..cc7ab5d260 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContentLegacy.kt @@ -0,0 +1,168 @@ +package com.tangem.feature.wallet.child.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +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.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.decorations.roundedShapeItemDecoration +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.TangemThemePreview +import com.tangem.core.res.R as ResR + +@Composable +internal fun AddAndManageBottomSheetContentLegacy( + onAddTokensClick: () -> Unit, + shouldShowOrganizeButton: Boolean, + onOrganizeTokensClick: () -> Unit, + onDismiss: () -> Unit, +) { + val config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = AddAndManageBottomSheetConfigContent, + ) + + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.primary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(ResR.string.main_add_and_manage_tokens), + endIconRes = R.drawable.ic_close_24, + onEndClick = onDismiss, + ) + }, + content = { + AddAndManageContent( + onAddTokensClick = onAddTokensClick, + shouldShowOrganizeButton = shouldShowOrganizeButton, + onOrganizeTokensClick = onOrganizeTokensClick, + ) + }, + ) +} + +@Composable +private fun AddAndManageContent( + onAddTokensClick: () -> Unit, + shouldShowOrganizeButton: Boolean, + onOrganizeTokensClick: () -> Unit, +) { + Column( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + AddAndManageRow( + iconRes = R.drawable.ic_plus_24, + title = ResR.string.add_and_manage_sheet_manage_title, + subtitle = ResR.string.add_and_manage_sheet_manage_subtitle, + onClick = onAddTokensClick, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = if (shouldShowOrganizeButton) 1 else 0, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + if (shouldShowOrganizeButton) { + AddAndManageRow( + iconRes = R.drawable.ic_filter_default_24, + title = ResR.string.add_and_manage_sheet_organize_title, + subtitle = ResR.string.add_and_manage_sheet_organize_subtitle, + onClick = onOrganizeTokensClick, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 1, + lastIndex = 1, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } + } +} + +@Composable +private fun AddAndManageRow( + iconRes: Int, + title: Int, + subtitle: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)), + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(id = title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(id = subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +private object AddAndManageBottomSheetConfigContent : TangemBottomSheetConfigContent + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun AddAndManageBottomSheetContent_Preview() { + TangemThemePreview { + AddAndManageContent( + onAddTokensClick = {}, + shouldShowOrganizeButton = true, + onOrganizeTokensClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt index ebba1ec794..d2442aa445 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizeAccountItemConverter.kt @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.child.organizetokens.model.converter.items +import com.tangem.common.ui.account.AccountIconItemStateConverter import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat @@ -20,6 +23,10 @@ internal class OrganizeAccountItemConverter( return OrganizeRowItemUM.Portfolio( headerRowUM = TangemHeaderRowUM( id = value.accountId.value, + startIconUM = TangemIconUM.Currency( + currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.RedesignExtraSmall) + .convert(value.account), + ), title = value.account.accountName.toUM().value, subtitle = stringReference( accountBalance?.amount.format { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt index 013b583609..562d21f546 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -99,7 +99,7 @@ internal fun WalletBalance( } } SpacerH(TangemTheme.dimens2.x2) - ActionButtons(buttons) + ActionButtons(buttons, modifier = Modifier.fillMaxWidth()) SpacerH(TangemTheme.dimens2.x6) } } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8b7ac52f4f..76bd3373b5 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1586" +tangemBlockchainSdk = "develop-1592" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-630" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^