Updated on 2026-08-14
This commit is contained in:
commit
216796ed09
27 changed files with 638 additions and 222 deletions
|
|
@ -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<TangemButtonUM>, 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NotificationUM> {
|
||||
// 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<NotificationUM>.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<NotificationUM>.maybeAddExceedsBalanceNotification(
|
||||
private fun MutableList<NotificationUM>.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<NotificationUM>.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<NotificationUM>.addTronNetworkFeesNotification(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<NotificationUM.Solana.RentInfo>()).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<NotificationUM.Error.ExistentialDeposit>()).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<NotificationUM.Error.MinimumAmountError>()).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<NotificationUM.Cardano.MinAdaValueCharged>()).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<NotificationUM.Warning.FeeCoverageNotification>()).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<SwapNotificationUM.Warning.NeedReserveToCreateAccount>()
|
||||
|
|
@ -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<SwapNotificationUM.Warning.ReduceAmount>()).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<NotificationUM.Error.TokenExceedsBalance>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<NotificationUM.Warning.TronAccountNotActivated>()
|
||||
|
|
@ -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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<NotificationUM.Warning.FeeTooLow>()).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<NotificationUM.Warning.FeeTooLow>()).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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AddAndManageBottomSheetConfigContent>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<AddAndManageBottomSheetConfigContent>(
|
||||
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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ internal fun WalletBalance(
|
|||
}
|
||||
}
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
ActionButtons(buttons)
|
||||
ActionButtons(buttons, modifier = Modifier.fillMaxWidth())
|
||||
SpacerH(TangemTheme.dimens2.x6)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue