From a9b5490cc43d1dc4c02bcbb3f3e91a036d1336c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 13:12:57 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/tokenaction/TokenActionRow.kt | 174 +++++++++++ .../ui/ds/button/SecondaryTangemButton.kt | 3 + .../core/ui/ds/button/TangemButtonInternal.kt | 16 +- .../core/ui/ds/button/TangemButtonUM.kt | 2 + .../core/ui/ds/button/action/ActionButtons.kt | 1 + .../tangem/core/ui/extensions/ModifierExt.kt | 26 ++ .../model/ScenarioUnavailabilityReason.kt | 5 +- .../ui/TokenActionsContentV2.kt | 92 +----- .../DefaultTokenDetailsComponent.kt | 10 + .../model/TokenDetailsClickIntents.kt | 16 + .../tokendetails/model/TokenDetailsModel.kt | 88 +++++- .../route/TokenDetailsBottomSheetConfig.kt | 6 + .../tokendetails/state/AddFundsUM.kt | 34 +++ .../state/TokenDetailsBalanceBlockUM.kt | 39 ++- .../state/TokenDetailsStateController.kt | 40 ++- .../tokendetails/state/TokenDetailsUM.kt | 3 + .../tokendetails/state/TransferUM.kt | 33 ++ .../state/ZeroBalanceActionsUM.kt | 34 +++ .../BindAddFundsActionButtonTransformer.kt | 23 ++ .../BindTransferActionButtonTransformer.kt | 20 ++ .../SetBalanceLoadingTransformer.kt | 4 +- .../transformer/SetBalanceTransformer.kt | 19 +- .../UpdateActionButtonsTransformer.kt | 39 +++ .../transformer/UpdateAddFundsTransformer.kt | 63 ++++ .../transformer/UpdateTransferTransformer.kt | 59 ++++ .../UpdateZeroBalanceActionsTransformer.kt | 47 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 29 +- .../AddFundsBottomSheetComponent.kt | 59 ++++ .../bottomsheet/AddFundsBottomSheetContent.kt | 165 ++++++++++ .../TransferBottomSheetComponent.kt | 59 ++++ .../bottomsheet/TransferBottomSheetContent.kt | 164 ++++++++++ .../ui/components/TokenDetailsBalanceBlock.kt | 101 ++++-- .../ui/components/ZeroBalanceActionsBlock.kt | 113 +++++++ ...BindAddFundsActionButtonTransformerTest.kt | 138 +++++++++ ...BindTransferActionButtonTransformerTest.kt | 123 ++++++++ ...ializeWithCryptoCurrencyTransformerTest.kt | 22 +- .../SetBalanceLoadingTransformerTest.kt | 45 ++- .../transformer/SetBalanceTransformerTest.kt | 78 ++++- .../SetTopBarTitleTransformerTest.kt | 6 + .../ToggleBalanceTypeTransformerTest.kt | 36 ++- .../UpdateAddFundsTransformerTest.kt | 288 ++++++++++++++++++ .../UpdateNotificationsTransformerTest.kt | 6 + ...pdateStakingNotificationTransformerTest.kt | 6 + .../UpdateTopBarMenuTransformerTest.kt | 6 + .../UpdateTransferTransformerTest.kt | 271 ++++++++++++++++ ...UpdateZeroBalanceActionsTransformerTest.kt | 231 ++++++++++++++ 46 files changed, 2679 insertions(+), 163 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokenaction/TokenActionRow.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/AddFundsUM.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/ZeroBalanceActionsUM.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateActionButtonsTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetComponent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetContent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetComponent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/ZeroBalanceActionsBlock.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokenaction/TokenActionRow.kt b/common/ui/src/main/java/com/tangem/common/ui/tokenaction/TokenActionRow.kt new file mode 100644 index 0000000000..487f87db7b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokenaction/TokenActionRow.kt @@ -0,0 +1,174 @@ +package com.tangem.common.ui.tokenaction + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.TangemTheme + +private const val ACTION_BACKGROUND_ALPHA = .1f + +/** + * Single-row token action ("Buy", "Receive", etc.) with accent icon, title, description and a + * customizable tail. Used in bottom sheets like Get Token / Add Funds and Add To Portfolio. + * + * @param iconRes leading 20dp icon drawn over an accent-colored circle + * @param title row primary text + * @param description row secondary text + * @param onClick single-click callback; row is non-interactive if `null` + * @param onLongClick long-press callback; pass `null` to disable long-press + * @param isEnabled when `false`, the row uses disabled-tier colors and ignores clicks + * @param tailContent content placed at the row's end. Defaults to a chevron-right icon. + */ +@Composable +fun TokenActionRow( + @DrawableRes iconRes: Int, + title: TextReference, + description: TextReference, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, + isEnabled: Boolean = true, + tailContent: @Composable () -> Unit = { DefaultTokenActionRowChevron(isEnabled = isEnabled) }, +) { + val hapticManager = LocalHapticManager.current + val accentColor = accentColor(isEnabled) + TangemRowContainer( + modifier = modifier + .background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ) + .clickableWithHaptic( + onClick = onClick.takeIf { isEnabled }, + onLongClick = onLongClick.takeIf { isEnabled }, + hapticManager = hapticManager, + ), + ) { + LeadingIcon(iconRes = iconRes, accentColor = accentColor) + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + text = title.resolveReference(), + style = TangemTheme.typography2.bodyMedium16, + color = titleColor(isEnabled), + ) + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + text = description.resolveReference(), + style = TangemTheme.typography2.captionMedium12, + color = descriptionColor(isEnabled), + ) + Tail { tailContent() } + } +} + +@Composable +private fun LeadingIcon(@DrawableRes iconRes: Int, accentColor: Color) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x3) + .size(40.dp) + .background( + color = accentColor.copy(alpha = ACTION_BACKGROUND_ALPHA), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(id = iconRes), + contentDescription = null, + tint = accentColor, + ) + } +} + +@Composable +private fun Tail(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.TAIL) + .padding(start = TangemTheme.dimens2.x2) + .size(24.dp), + contentAlignment = Alignment.Center, + ) { + content() + } +} + +@Composable +private fun accentColor(isEnabled: Boolean): Color = if (isEnabled) { + TangemTheme.colors2.graphic.status.accent +} else { + TangemTheme.colors2.graphic.neutral.quaternary +} + +@Composable +private fun titleColor(isEnabled: Boolean): Color = if (isEnabled) { + TangemTheme.colors2.text.neutral.primary +} else { + TangemTheme.colors2.text.status.disabled +} + +@Composable +private fun descriptionColor(isEnabled: Boolean): Color = if (isEnabled) { + TangemTheme.colors2.text.neutral.secondary +} else { + TangemTheme.colors2.text.status.disabled +} + +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.clickableWithHaptic( + onClick: (() -> Unit)?, + onLongClick: (() -> Unit)?, + hapticManager: HapticManager, +): Modifier { + if (onClick == null) return this + return combinedClickable( + onClick = hapticManager.withHaptic(TangemHapticEffect.View.SegmentTick, onClick), + onLongClick = onLongClick?.let { hapticManager.withHaptic(TangemHapticEffect.View.LongPress, it) }, + ) +} + +private fun HapticManager.withHaptic(effect: TangemHapticEffect, action: () -> Unit): () -> Unit = { + perform(effect) + action() +} + +@Composable +private fun DefaultTokenActionRowChevron(isEnabled: Boolean) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + tint = if (isEnabled) { + TangemTheme.colors2.graphic.neutral.tertiary + } else { + TangemTheme.colors2.graphic.neutral.quaternary + }, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt index b6ced0646b..7ef68a6a28 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt @@ -39,6 +39,7 @@ fun SecondaryTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifie isLoading = buttonUM.isLoading, size = buttonUM.size, shape = buttonUM.shape, + onLongClick = buttonUM.onLongClick, ) } @@ -68,6 +69,7 @@ fun SecondaryTangemButton( isLoading: Boolean = false, size: TangemButtonSize = TangemButtonSize.X15, shape: TangemButtonShape = TangemButtonShape.Default, + onLongClick: (() -> Unit)? = null, ) { val backgroundModifier = if (isEnabled) { Modifier.background(TangemTheme.colors2.button.backgroundSecondary) @@ -93,6 +95,7 @@ fun SecondaryTangemButton( isLoading = isLoading, size = size, iconPosition = iconPosition, + onLongClick = onLongClick, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index f1abe125cf..35206c39f3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -67,12 +67,13 @@ internal fun TangemButtonInternal( hasPadding: Boolean = true, contentColor: Color = TangemTheme.colors2.text.neutral.primary, size: TangemButtonSize = TangemButtonSize.X15, + onLongClick: (() -> Unit)? = null, ) { ProvideButtonRippleConfiguration { Box( modifier = modifier .testTag(BaseButtonTestTags.BUTTON) - .clickableSingle(enabled = isEnabled, onClick = onClick, role = Role.Button) + .buttonClickable(isEnabled = isEnabled, onClick = onClick, onLongClick = onLongClick) .heightIn(min = size.toHeightDp()) .conditionalCompose(text == null) { width(size.toHeightDp()) @@ -181,6 +182,19 @@ private fun ButtonContent( } } +private fun Modifier.buttonClickable(isEnabled: Boolean, onClick: () -> Unit, onLongClick: (() -> Unit)?): Modifier { + return if (onLongClick != null) { + combinedClickableSingle( + enabled = isEnabled, + role = Role.Button, + onClick = onClick, + onLongClick = onLongClick, + ) + } else { + clickableSingle(enabled = isEnabled, onClick = onClick, role = Role.Button) + } +} + @Composable private inline fun ProvideButtonRippleConfiguration(crossinline content: @Composable () -> Unit) { CompositionLocalProvider( diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt index 04c7a63e72..244162936d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.TextReference * @param shape TangemButtonShape defining the shape of the button. * @param type TangemButtonType defining the style type of the button. * @param onClick Lambda to be invoked when the button is clicked. + * @param onLongClick Lambda to be invoked when the button is long-clicked. * [REDACTED_AUTHOR] */ @@ -32,6 +33,7 @@ data class TangemButtonUM( val shape: TangemButtonShape = TangemButtonShape.Default, val type: TangemButtonType, val onClick: () -> Unit, + val onLongClick: (() -> Unit)? = null, ) /** Enum class representing the style types of Tangem buttons */ 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 c7a46bcb1e..fc7c6c2af8 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 @@ -60,6 +60,7 @@ fun ActionButtons(buttons: ImmutableList, modifier: Modifier = M onClick = button.onClick, isEnabled = button.isEnabled, shape = TangemButtonShape.Rounded, + onLongClick = button.onLongClick, ) Text( text = button.text.orEmpty().resolveReference(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index 1f0a7b1208..f34122b0dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.extensions import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -37,6 +38,31 @@ fun Modifier.clickableSingle( ) } +/** + * Combined clickable modifier that debounces multiple [onClick] events in a short period of time. + * Mirrors [clickableSingle] but also exposes [onLongClick]; long-press is not debounced. + */ +fun Modifier.combinedClickableSingle( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onLongClickLabel: String? = null, + onLongClick: (() -> Unit)? = null, + onClick: () -> Unit, +) = composed { + val multipleEventsCutter = remember { MultipleClickPreventer.get() } + Modifier.combinedClickable( + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + onLongClickLabel = onLongClickLabel, + onLongClick = onLongClick, + onClick = { multipleEventsCutter.processEvent { onClick() } }, + indication = LocalIndication.current, + interactionSource = remember { MutableInteractionSource() }, + ) +} + /** * Conditionally applies a modifier based on a boolean condition. */ diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 64053517d7..293ef64900 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -61,4 +61,7 @@ sealed class ScenarioUnavailabilityReason { enum class WithdrawalScenario { SELL, SEND // TODO staking create&process STAKING } -} \ No newline at end of file +} + +val ScenarioUnavailabilityReason.isLoading: Boolean + get() = this == ScenarioUnavailabilityReason.DataLoading || this is ScenarioUnavailabilityReason.ExpressLoading \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt index d72a9f324c..3dcf54ba9c 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt @@ -1,13 +1,9 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable 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 import androidx.compose.runtime.CompositionLocalProvider @@ -15,9 +11,6 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.layoutId -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -26,6 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.common.ui.markets.action.QuickActionUM import com.tangem.common.ui.markets.action.QuickActions +import com.tangem.common.ui.tokenaction.TokenActionRow import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState @@ -36,13 +30,10 @@ import com.tangem.core.ui.ds.button.TangemButtonShape import com.tangem.core.ui.ds.button.TangemButtonSize import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.TangemDeviceIcon -import com.tangem.core.ui.ds.row.TangemRowContainer -import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.formatStyled import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.* import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.PortfolioBadgeUM @@ -52,8 +43,6 @@ import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal import java.util.UUID -private const val ACTION_BACKGROUND_ALPHA = .1f - @Composable internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = Modifier) { Column( @@ -72,10 +61,13 @@ internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = M ) { state.quickActions.actions.fastForEach { actionUM -> key(actionUM.title) { - ActionRow( - state = actionUM, + TokenActionRow( + iconRes = actionUM.icon, + title = actionUM.title, + description = actionUM.description, onClick = { state.quickActions.onQuickActionClick(actionUM) }, - onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) }, + onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) } + .takeIf { actionUM.isLongClickAvailable }, ) } } @@ -95,76 +87,6 @@ internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = M } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun ActionRow( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit), - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - - TangemRowContainer( - modifier = modifier - .combinedClickable( - onLongClick = onLongClickInternal.takeIf { state.isLongClickAvailable }, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .background( - color = TangemTheme.colors2.surface.level3, - shape = RoundedCornerShape(TangemTheme.dimens2.x5), - ), - ) { - Box( - modifier = Modifier - .layoutId(TangemRowLayoutId.HEAD) - .padding(end = TangemTheme.dimens2.x3) - .size(40.dp) - .background( - color = TangemTheme.colors2.graphic.status.accent.copy(alpha = ACTION_BACKGROUND_ALPHA), - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors2.graphic.status.accent, - ) - } - Text( - modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), - text = state.title.resolveReference(), - style = TangemTheme.typography2.bodyMedium16, - color = TangemTheme.colors2.text.neutral.primary, - ) - Text( - modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), - text = state.description.resolveReference(), - style = TangemTheme.typography2.captionMedium12, - color = TangemTheme.colors2.text.neutral.secondary, - ) - Icon( - modifier = Modifier - .layoutId(TangemRowLayoutId.TAIL) - .padding(start = TangemTheme.dimens2.x2) - .size(24.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), - tint = TangemTheme.colors2.graphic.neutral.tertiary, - contentDescription = null, - ) - } -} - @Composable private fun TokenHeader( addedToken: TokenItemState, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 733b61a397..f6ab5f2511 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -21,9 +21,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.AddFundsBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.TokenDetailsComponent @@ -159,6 +161,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( dynamicAddressesDelegate = model.dynamicAddressesDelegate, onDismiss = model.bottomSheetNavigation::dismiss, ) + is TokenDetailsBottomSheetConfig.AddFunds -> AddFundsBottomSheetComponent( + stateFlow = model.addFundsUiState, + onDismiss = model.bottomSheetNavigation::dismiss, + ) + is TokenDetailsBottomSheetConfig.Transfer -> TransferBottomSheetComponent( + stateFlow = model.transferUiState, + onDismiss = model.bottomSheetNavigation::dismiss, + ) } @AssistedFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index 0fd56bb881..2500a2bdd5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -19,8 +19,16 @@ interface TokenDetailsClickIntents { fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) + + fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onAddFundsClick() + + fun onTransferClick() + fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onHideClick() @@ -104,6 +112,10 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents { override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + override fun onAddFundsClick() { /* no op */ } + + override fun onTransferClick() { /* no op */ } + override fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) { /* no op */ } override fun onStakeBannerClick() { /* no op */ } @@ -126,6 +138,10 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents { override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + + override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + override fun onHideClick() { /* no op */ } override fun onHideConfirmed() { /* no op */ } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 2b86f0895c..71859ae320 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -90,13 +90,21 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsStateController import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateActionButtonsTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateAddFundsTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTransferTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateZeroBalanceActionsTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.BindAddFundsActionButtonTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.BindTransferActionButtonTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetTopBarTitleTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.ToggleBalanceTypeTransformer import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateStakingNotificationTransformer @@ -217,6 +225,24 @@ internal class TokenDetailsModel @Inject constructor( val redesignUiState: StateFlow get() = redesignStateController.uiState + val addFundsUiState: StateFlow + field = redesignStateController.uiState + .map { it.addFundsUM } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = redesignStateController.value.addFundsUM, + ) + + val transferUiState: StateFlow + field = redesignStateController.uiState + .map { it.transferUM } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = redesignStateController.value.transferUM, + ) + // region Clore migration // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -313,6 +339,34 @@ internal class TokenDetailsModel @Inject constructor( .onEach { state -> sendButtonsEvents(state.states) uiState.value = stateFactory.getManageButtonsState(actions = state.states) + if (designFeatureToggles.isRedesignEnabled) { + redesignStateController.update( + UpdateActionButtonsTransformer( + actions = state.states, + clickIntents = this@TokenDetailsModel, + ), + ) + redesignStateController.update( + UpdateAddFundsTransformer( + actions = state.states, + clickIntents = this@TokenDetailsModel, + onActionDispatched = bottomSheetNavigation::dismiss, + ), + ) + redesignStateController.update( + UpdateTransferTransformer( + actions = state.states, + clickIntents = this@TokenDetailsModel, + onActionDispatched = bottomSheetNavigation::dismiss, + ), + ) + redesignStateController.update( + UpdateZeroBalanceActionsTransformer( + actions = state.states, + clickIntents = this@TokenDetailsModel, + ), + ) + } } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -477,6 +531,14 @@ internal class TokenDetailsModel @Inject constructor( router.popBackStack() } + override fun onAddFundsClick() { + bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.AddFunds) + } + + override fun onTransferClick() { + bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer) + } + override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send( TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy( @@ -660,6 +722,22 @@ internal class TokenDetailsModel @Inject constructor( } override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { + handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.ANY, checkYieldSupply = true) + } + + override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { + handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true) + } + + override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { + handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.TO, checkYieldSupply = false) + } + + private fun handleSwap( + unavailabilityReason: ScenarioUnavailabilityReason, + currencyPosition: AppRoute.Swap.CurrencyPosition, + checkYieldSupply: Boolean, + ) { analyticsEventsHandler.send( TokenScreenAnalyticsEvent.ButtonWithParams.ButtonExchange( token = cryptoCurrency.symbol, @@ -674,7 +752,7 @@ internal class TokenDetailsModel @Inject constructor( } modelScope.launch { - if (needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) { + if (checkYieldSupply && needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) { bottomSheetNavigation.activate( configuration = TokenDetailsBottomSheetConfig.YieldSupplyWarning( cryptoCurrency = cryptoCurrency, @@ -687,6 +765,7 @@ internal class TokenDetailsModel @Inject constructor( cryptoCurrency = cryptoCurrency, userWalletId = userWalletId, screenSource = AnalyticsParam.ScreensSources.Token.value, + currencyPosition = currencyPosition, ), ) } @@ -1284,6 +1363,13 @@ internal class TokenDetailsModel @Inject constructor( onRefreshSwipe = ::onRefreshSwipe, ), ) + redesignStateController.update( + BindAddFundsActionButtonTransformer( + onClick = ::onAddFundsClick, + onLongClick = { onCopyAddress() }, + ), + ) + redesignStateController.update(BindTransferActionButtonTransformer(onClick = ::onTransferClick)) } private fun observeRedesignTopBarTitle() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt index 071fa53e51..e3ddd4ecb6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt @@ -30,4 +30,10 @@ sealed class TokenDetailsBottomSheetConfig : Route { @Serializable data object DynamicAddresses : TokenDetailsBottomSheetConfig() + + @Serializable + data object AddFunds : TokenDetailsBottomSheetConfig() + + @Serializable + data object Transfer : TokenDetailsBottomSheetConfig() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/AddFundsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/AddFundsUM.kt new file mode 100644 index 0000000000..733104d768 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/AddFundsUM.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +/** + * State of the "Get token" bottom sheet shown after tapping the balance-block "Add funds" button. + * + * Stays [Loading] while [com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase] hasn't yet + * emitted the action list; the sheet renders a spinner in the tail of each row. Once actions + * arrive the state becomes [Content]; unavailable actions stay visible but with + * [Row.isEnabled] = false. Rows whose action is absent from the response are dropped (null). + */ +@Immutable +internal sealed interface AddFundsUM : TangemBottomSheetConfigContent { + + @Immutable + data object Loading : AddFundsUM + + @Immutable + data class Content( + val buy: Row?, + val swap: Row?, + val receive: Row?, + ) : AddFundsUM + + @Immutable + data class Row( + val isLoading: Boolean, + val isEnabled: Boolean, + val onClick: () -> Unit, + val onLongClick: (() -> Unit)? = null, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt index a4cfef3e52..406042f571 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt @@ -11,18 +11,24 @@ import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class TokenDetailsBalanceBlockUM { - abstract val actionButtons: ImmutableList + abstract val addFundsButton: TangemButtonUM + abstract val swapButton: TangemButtonUM + abstract val transferButton: TangemButtonUM abstract val tokenBalanceTypeUM: TokenBalanceTypeUM abstract val currencyIconState: CurrencyIconState data class Loading( - override val actionButtons: ImmutableList, + override val addFundsButton: TangemButtonUM, + override val swapButton: TangemButtonUM, + override val transferButton: TangemButtonUM, override val tokenBalanceTypeUM: TokenBalanceTypeUM, override val currencyIconState: CurrencyIconState, ) : TokenDetailsBalanceBlockUM() data class Content( - override val actionButtons: ImmutableList, + override val addFundsButton: TangemButtonUM, + override val swapButton: TangemButtonUM, + override val transferButton: TangemButtonUM, override val tokenBalanceTypeUM: TokenBalanceTypeUM, override val currencyIconState: CurrencyIconState, val displayCryptoBalanceAll: TextReference, @@ -30,6 +36,7 @@ internal sealed class TokenDetailsBalanceBlockUM { val displayCryptoBalanceAvailable: TextReference?, val displayFiatBalanceAvailable: TextReference?, val isBalanceFlickering: Boolean, + val isBalanceZero: Boolean, ) : TokenDetailsBalanceBlockUM() { val displayCryptoBalance: TextReference @@ -46,7 +53,9 @@ internal sealed class TokenDetailsBalanceBlockUM { } data class Error( - override val actionButtons: ImmutableList, + override val addFundsButton: TangemButtonUM, + override val swapButton: TangemButtonUM, + override val transferButton: TangemButtonUM, override val tokenBalanceTypeUM: TokenBalanceTypeUM, override val currencyIconState: CurrencyIconState, ) : TokenDetailsBalanceBlockUM() @@ -58,6 +67,28 @@ internal sealed class TokenDetailsBalanceBlockUM { is Loading -> this.copy(currencyIconState = iconState) } } + + fun copyButtons( + addFundsButton: TangemButtonUM = this.addFundsButton, + swapButton: TangemButtonUM = this.swapButton, + transferButton: TangemButtonUM = this.transferButton, + ): TokenDetailsBalanceBlockUM = when (this) { + is Content -> copy( + addFundsButton = addFundsButton, + swapButton = swapButton, + transferButton = transferButton, + ) + is Error -> copy( + addFundsButton = addFundsButton, + swapButton = swapButton, + transferButton = transferButton, + ) + is Loading -> copy( + addFundsButton = addFundsButton, + swapButton = swapButton, + transferButton = transferButton, + ) + } } internal sealed class TokenBalanceTypeUM { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt index 6175a1fdd6..ce9ba16f45 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokendetails.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf @@ -42,21 +43,29 @@ internal class TokenDetailsStateController @Inject constructor() { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf( - TangemButtonUM( - text = resourceReference(R.string.tangempay_card_details_add_funds), - tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_down_24), - onClick = { }, - isEnabled = true, - type = TangemButtonType.Secondary, - ), - TangemButtonUM( - text = resourceReference(R.string.common_transfer), - tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_up_24), - onClick = { }, - isEnabled = true, - type = TangemButtonType.Secondary, + addFundsButton = TangemButtonUM( + text = resourceReference(R.string.tangempay_card_details_add_funds), + tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_down_24), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, + ), + swapButton = TangemButtonUM( + text = resourceReference(R.string.common_swap), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_exchange_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.quaternary }, ), + onClick = { }, + isEnabled = false, + type = TangemButtonType.Secondary, + ), + transferButton = TangemButtonUM( + text = resourceReference(R.string.common_transfer), + tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_up_24), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, ), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, @@ -70,6 +79,9 @@ internal class TokenDetailsStateController @Inject constructor() { ), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt index f67bd6b402..55220af5a7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt @@ -21,6 +21,9 @@ internal data class TokenDetailsUM( val pullToRefreshConfig: PullToRefreshConfig, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, + val addFundsUM: AddFundsUM, + val transferUM: TransferUM, + val zeroBalanceActionsUM: ZeroBalanceActionsUM, ) @Immutable diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt new file mode 100644 index 0000000000..7f7d1ee2b9 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +/** + * State of the "Transfer" bottom sheet shown after tapping the balance-block "Transfer" button. + * + * Stays [Loading] while [com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase] hasn't yet + * emitted the action list; the sheet renders a spinner in the tail of each row. Once actions + * arrive the state becomes [Content]; unavailable actions stay visible but with + * [Row.isEnabled] = false. Rows whose action is absent from the response are dropped (null). + */ +@Immutable +internal sealed interface TransferUM : TangemBottomSheetConfigContent { + + @Immutable + data object Loading : TransferUM + + @Immutable + data class Content( + val send: Row?, + val swap: Row?, + val sell: Row?, + ) : TransferUM + + @Immutable + data class Row( + val isLoading: Boolean, + val isEnabled: Boolean, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/ZeroBalanceActionsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/ZeroBalanceActionsUM.kt new file mode 100644 index 0000000000..d713b0e3e9 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/ZeroBalanceActionsUM.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Immutable + +/** + * State of the Buy / Swap / Receive rows rendered in place of the balance-block action buttons + * when the token balance is zero. + * + * Stays [Loading] while [com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase] hasn't yet + * emitted the action list. Once actions arrive the state becomes [Content], and each row + * carries [Row.isEnabled] reflecting its current `ScenarioUnavailabilityReason`. Disabled rows + * stay visible but ignore clicks. + */ +@Immutable +internal sealed interface ZeroBalanceActionsUM { + + @Immutable + data object Loading : ZeroBalanceActionsUM + + @Immutable + data class Content( + val buy: Row?, + val swap: Row?, + val receive: Row?, + ) : ZeroBalanceActionsUM + + @Immutable + data class Row( + val isLoading: Boolean, + val isEnabled: Boolean, + val onClick: () -> Unit, + val onLongClick: (() -> Unit)? = null, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformer.kt new file mode 100644 index 0000000000..39444af84f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +/** + * Wires the balance block's "Add funds" button click handlers to the provided actions. + * + * [TokenDetailsStateController.getInitialState] sets up the button without click handlers + * because the controller can't see [TokenDetailsClickIntents]; this transformer fills them in + * once the model is constructed. + */ +internal class BindAddFundsActionButtonTransformer( + private val onClick: () -> Unit, + private val onLongClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val prev = prevState.balanceBlockUM + val updated = prev.addFundsButton.copy(onClick = onClick, onLongClick = onLongClick) + return prevState.copy(balanceBlockUM = prev.copyButtons(addFundsButton = updated)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformer.kt new file mode 100644 index 0000000000..9b8a5f0880 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +/** + * Wires the balance block's "Transfer" button onClick to the provided action. + * + * See [BindAddFundsActionButtonTransformer] for the same pattern used for the "Add funds" button. + */ +internal class BindTransferActionButtonTransformer( + private val onClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val prev = prevState.balanceBlockUM + val updated = prev.transferButton.copy(onClick = onClick) + return prevState.copy(balanceBlockUM = prev.copyButtons(transferButton = updated)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt index 5b4852a7e3..cab235c7ab 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt @@ -14,7 +14,9 @@ internal class SetBalanceLoadingTransformer( val prevBalance = prevState.balanceBlockUM return prevState.copy( balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = prevBalance.actionButtons, + addFundsButton = prevBalance.addFundsButton, + swapButton = prevBalance.swapButton, + transferButton = prevBalance.transferButton, tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = currencyIconState, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt index 28da2953eb..e2f6f7f828 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt @@ -40,7 +40,9 @@ internal class SetBalanceTransformer( val prev = prevState.balanceBlockUM val balanceBlockUM = when (status.value) { is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockUM.Loading( - actionButtons = prev.actionButtons, + addFundsButton = prev.addFundsButton, + swapButton = prev.swapButton, + transferButton = prev.transferButton, tokenBalanceTypeUM = prev.tokenBalanceTypeUM, currencyIconState = prev.currencyIconState, ) @@ -53,7 +55,9 @@ internal class SetBalanceTransformer( is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, -> TokenDetailsBalanceBlockUM.Error( - actionButtons = prev.actionButtons, + addFundsButton = prev.addFundsButton, + swapButton = prev.swapButton, + transferButton = prev.transferButton, tokenBalanceTypeUM = prev.tokenBalanceTypeUM, currencyIconState = prev.currencyIconState, ) @@ -80,16 +84,18 @@ internal class SetBalanceTransformer( TokenBalanceTypeUM.Single } + val totalCryptoAmount = computeTotal(status.value.amount, stakingCryptoAmount) + return TokenDetailsBalanceBlockUM.Content( - actionButtons = prev.actionButtons, + addFundsButton = prev.addFundsButton, + swapButton = prev.swapButton, + transferButton = prev.transferButton, currencyIconState = prev.currencyIconState, tokenBalanceTypeUM = tokenBalanceTypeUM, displayFiatBalanceAll = formatFiatStyled( fiatAmount = computeTotal(status.value.fiatAmount, stakingFiatAmount), ), - displayCryptoBalanceAll = formatCrypto( - amount = computeTotal(status.value.amount, stakingCryptoAmount), - ), + displayCryptoBalanceAll = formatCrypto(amount = totalCryptoAmount), displayFiatBalanceAvailable = if (hasStaking) { formatFiatStyled(fiatAmount = status.value.fiatAmount) } else { @@ -101,6 +107,7 @@ internal class SetBalanceTransformer( null }, isBalanceFlickering = status.value.sources.total == StatusSource.CACHE, + isBalanceZero = totalCryptoAmount.isNullOrZero(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateActionButtonsTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateActionButtonsTransformer.kt new file mode 100644 index 0000000000..77e38e9525 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateActionButtonsTransformer.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateActionButtonsTransformer( + private val actions: List, + private val clickIntents: TokenDetailsClickIntents, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val swapAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Swap } + ?: return prevState + + val prev = prevState.balanceBlockUM + val isSwapEnabled = swapAction.unavailabilityReason == ScenarioUnavailabilityReason.None + + val updated = prev.swapButton.copy( + isEnabled = isSwapEnabled, + tangemIconUM = (prev.swapButton.tangemIconUM as? TangemIconUM.Icon)?.copy( + tint = { + if (isSwapEnabled) { + TangemTheme.colors2.graphic.neutral.primary + } else { + TangemTheme.colors2.graphic.neutral.quaternary + } + }, + ) ?: prev.swapButton.tangemIconUM, + onClick = { clickIntents.onSwapFromClick(swapAction.unavailabilityReason) }, + ) + + return prevState.copy(balanceBlockUM = prev.copyButtons(swapButton = updated)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformer.kt new file mode 100644 index 0000000000..0252ad38a3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformer.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.isLoading +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateAddFundsTransformer( + private val actions: List, + private val clickIntents: TokenDetailsClickIntents, + private val onActionDispatched: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val buyAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Buy } + val swapAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Swap } + val receiveAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Receive } + + if (buyAction == null && swapAction == null && receiveAction == null) return prevState + + val buyRow = buyAction?.let { action -> + AddFundsUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onBuyClick(action.unavailabilityReason) + }, + ) + } + val swapRow = swapAction?.let { action -> + AddFundsUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onSwapToClick(action.unavailabilityReason) + }, + ) + } + val receiveRow = receiveAction?.let { action -> + AddFundsUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onReceiveClick(action.unavailabilityReason) + }, + onLongClick = { + onActionDispatched() + clickIntents.onCopyAddress() + }, + ) + } + + return prevState.copy( + addFundsUM = AddFundsUM.Content(buy = buyRow, swap = swapRow, receive = receiveRow), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt new file mode 100644 index 0000000000..5e2b102c2c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.isLoading +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateTransferTransformer( + private val actions: List, + private val clickIntents: TokenDetailsClickIntents, + private val onActionDispatched: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val sendAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Send } + val swapAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Swap } + val sellAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Sell } + + if (sendAction == null && swapAction == null && sellAction == null) return prevState + + val sendRow = sendAction?.let { action -> + TransferUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onSendClick(action.unavailabilityReason) + }, + ) + } + val swapRow = swapAction?.let { action -> + TransferUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onSwapFromClick(action.unavailabilityReason) + }, + ) + } + val sellRow = sellAction?.let { action -> + TransferUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onSellClick(action.unavailabilityReason) + }, + ) + } + + return prevState.copy( + transferUM = TransferUM.Content(send = sendRow, swap = swapRow, sell = sellRow), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt new file mode 100644 index 0000000000..5ec1dd92d2 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.isLoading +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateZeroBalanceActionsTransformer( + private val actions: List, + private val clickIntents: TokenDetailsClickIntents, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val buyAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Buy } + val swapAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Swap } + val receiveAction = actions.firstNotNullOfOrNull { it as? TokenActionsState.ActionState.Receive } + + if (buyAction == null && swapAction == null && receiveAction == null) return prevState + + return prevState.copy( + zeroBalanceActionsUM = ZeroBalanceActionsUM.Content( + buy = buyAction?.toRow(onClick = clickIntents::onBuyClick), + swap = swapAction?.toRow(onClick = clickIntents::onSwapToClick), + receive = receiveAction?.toRow( + onClick = clickIntents::onReceiveClick, + onLongClick = { clickIntents.onCopyAddress() }, + ), + ), + ) + } + + private fun TokenActionsState.ActionState.toRow( + onClick: (ScenarioUnavailabilityReason) -> Unit, + onLongClick: (() -> Unit)? = null, + ): ZeroBalanceActionsUM.Row { + val reason = unavailabilityReason + return ZeroBalanceActionsUM.Row( + isLoading = reason.isLoading, + isEnabled = reason == ScenarioUnavailabilityReason.None, + onClick = { onClick(reason) }, + onLongClick = onLongClick, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index fcf5a4c294..c582b3c507 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -36,6 +36,8 @@ import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem @@ -47,12 +49,16 @@ import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.ZeroBalanceActionsBlock import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.txhistory.component.TxHistoryComponent @@ -194,6 +200,15 @@ private fun TokenDetailsBody( modifier = Modifier.fillMaxWidth(), ) } + val balance = tokenDetailsUM.balanceBlockUM + if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) { + item(key = "zero_balance_actions") { + ZeroBalanceActionsBlock( + state = tokenDetailsUM.zeroBalanceActionsUM, + modifier = itemModifier, + ) + } + } notifications( notifications = tokenDetailsUM.notifications, contentColor = rootBackground, @@ -253,7 +268,9 @@ private fun TokenDetailsScreen_Preview() { notifications = persistentListOf(), earnBlockState = null, balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), + addFundsButton = previewActionButton(), + swapButton = previewActionButton(), + transferButton = previewActionButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), @@ -264,6 +281,9 @@ private fun TokenDetailsScreen_Preview() { ), isBalanceHidden = false, isMarketPriceAvailable = true, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ), yieldSupplyComponent = object : YieldSupplyComponent { @Composable @@ -287,6 +307,13 @@ private fun TokenDetailsScreen_Preview() { } } +private fun previewActionButton(): TangemButtonUM = TangemButtonUM( + text = stringReference(""), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, +) + private val PreviewExpressTransactionsComponent = object : ExpressTransactionsComponent { override val state: StateFlow = MutableStateFlow( ExpressTransactionsBlockState( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetComponent.kt new file mode 100644 index 0000000000..4e06d5b4a9 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetComponent.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import kotlinx.coroutines.flow.StateFlow +import com.tangem.core.ui.R as CoreR + +internal class AddFundsBottomSheetComponent( + private val stateFlow: StateFlow, + private val onDismiss: () -> Unit, +) : ComposableBottomSheetComponent { + + override fun dismiss() { + onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by stateFlow.collectAsStateWithLifecycle() + + val config = remember(state) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state, + ) + } + + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors2.surface.level2, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(CoreR.string.common_get_token), + endIconRes = CoreR.drawable.ic_close_24, + onEndClick = ::dismiss, + ) + }, + content = { contentState -> + AddFundsBottomSheetContent( + state = contentState, + onCloseClick = ::dismiss, + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetContent.kt new file mode 100644 index 0000000000..c33d9e4a3c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/AddFundsBottomSheetContent.kt @@ -0,0 +1,165 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +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.dp +import com.tangem.common.ui.tokenaction.TokenActionRow +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalHazeState +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import dev.chrisbanes.haze.rememberHazeState +import com.tangem.core.ui.R as CoreR + +@Composable +internal fun AddFundsBottomSheetContent(state: AddFundsUM, onCloseClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + BuyActionRow(state = state) + SwapActionRow(state = state) + ReceiveActionRow(state = state) + + SpacerH(TangemTheme.dimens2.x2) + + CompositionLocalProvider(LocalHazeState provides rememberHazeState()) { + SecondaryTangemButton( + modifier = Modifier.fillMaxWidth(), + onClick = onCloseClick, + text = resourceReference(CoreR.string.common_close), + size = TangemButtonSize.X12, + shape = TangemButtonShape.Rounded, + ) + } + } +} + +@Composable +private fun BuyActionRow(state: AddFundsUM) { + val row = (state as? AddFundsUM.Content)?.buy + if (state is AddFundsUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_credit_card_20, + title = resourceReference(CoreR.string.common_buy), + description = resourceReference(CoreR.string.quick_action_buy_description), + row = row, + isLoading = state is AddFundsUM.Loading, + ) +} + +@Composable +private fun SwapActionRow(state: AddFundsUM) { + val row = (state as? AddFundsUM.Content)?.swap + if (state is AddFundsUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_exchange_mini_24, + title = resourceReference(CoreR.string.common_swap), + description = resourceReference(CoreR.string.quick_action_swap_description), + row = row, + isLoading = state is AddFundsUM.Loading, + ) +} + +@Composable +private fun ReceiveActionRow(state: AddFundsUM) { + val row = (state as? AddFundsUM.Content)?.receive + if (state is AddFundsUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_qrcode_new_24, + title = resourceReference(CoreR.string.common_receive), + description = resourceReference(CoreR.string.quick_action_receive_description), + row = row, + isLoading = state is AddFundsUM.Loading, + ) +} + +@Composable +private fun ActionRow( + iconRes: Int, + title: TextReference, + description: TextReference, + row: AddFundsUM.Row?, + isLoading: Boolean, +) { + if (isLoading || row?.isLoading == true) { + TokenActionRow( + iconRes = iconRes, + title = title, + description = description, + tailContent = { TailLoader() }, + ) + } else { + TokenActionRow( + iconRes = iconRes, + title = title, + description = description, + onClick = row?.onClick, + onLongClick = row?.onLongClick, + isEnabled = row?.isEnabled == true, + ) + } +} + +@Composable +private fun TailLoader() { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = TangemTheme.colors2.graphic.neutral.tertiary, + strokeWidth = 2.dp, + ) +} + +// region Preview +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview(@PreviewParameter(AddFundsPreviewProvider::class) state: AddFundsUM) { + TangemThemePreviewRedesign { + CompositionLocalProvider(LocalRedesignEnabled provides true) { + AddFundsBottomSheetContent( + state = state, + onCloseClick = {}, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} + +private class AddFundsPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AddFundsUM.Loading, + AddFundsUM.Content( + buy = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swap = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}), + receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}), + ), + AddFundsUM.Content( + buy = AddFundsUM.Row(isLoading = false, isEnabled = false, onClick = {}), + swap = AddFundsUM.Row(isLoading = false, isEnabled = false, onClick = {}), + receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}), + ), + AddFundsUM.Content( + buy = null, + swap = null, + receive = AddFundsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetComponent.kt new file mode 100644 index 0000000000..fe20b8a992 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetComponent.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import kotlinx.coroutines.flow.StateFlow +import com.tangem.core.ui.R as CoreR + +internal class TransferBottomSheetComponent( + private val stateFlow: StateFlow, + private val onDismiss: () -> Unit, +) : ComposableBottomSheetComponent { + + override fun dismiss() { + onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by stateFlow.collectAsStateWithLifecycle() + + val config = remember(state) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state, + ) + } + + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors2.surface.level2, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(CoreR.string.common_transfer), + endIconRes = CoreR.drawable.ic_close_24, + onEndClick = ::dismiss, + ) + }, + content = { contentState -> + TransferBottomSheetContent( + state = contentState, + onCloseClick = ::dismiss, + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt new file mode 100644 index 0000000000..cc699a02df --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt @@ -0,0 +1,164 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +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.dp +import com.tangem.common.ui.tokenaction.TokenActionRow +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalHazeState +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import dev.chrisbanes.haze.rememberHazeState +import com.tangem.core.ui.R as CoreR + +@Composable +internal fun TransferBottomSheetContent(state: TransferUM, onCloseClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + SendActionRow(state = state) + SwapActionRow(state = state) + SellActionRow(state = state) + + SpacerH(TangemTheme.dimens2.x2) + + CompositionLocalProvider(LocalHazeState provides rememberHazeState()) { + SecondaryTangemButton( + modifier = Modifier.fillMaxWidth(), + onClick = onCloseClick, + text = resourceReference(CoreR.string.common_close), + size = TangemButtonSize.X12, + shape = TangemButtonShape.Rounded, + ) + } + } +} + +@Composable +private fun SendActionRow(state: TransferUM) { + val row = (state as? TransferUM.Content)?.send + if (state is TransferUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_arrow_up_24, + title = resourceReference(CoreR.string.common_send), + description = resourceReference(CoreR.string.quick_action_send_description), + row = row, + isLoading = state is TransferUM.Loading, + ) +} + +@Composable +private fun SwapActionRow(state: TransferUM) { + val row = (state as? TransferUM.Content)?.swap + if (state is TransferUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_exchange_mini_24, + title = resourceReference(CoreR.string.common_swap), + description = resourceReference(CoreR.string.quick_action_swap_description), + row = row, + isLoading = state is TransferUM.Loading, + ) +} + +@Composable +private fun SellActionRow(state: TransferUM) { + val row = (state as? TransferUM.Content)?.sell + if (state is TransferUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_currency_24, + title = resourceReference(CoreR.string.common_sell), + description = resourceReference(CoreR.string.quick_action_sell_description), + row = row, + isLoading = state is TransferUM.Loading, + ) +} + +@Composable +private fun ActionRow( + iconRes: Int, + title: TextReference, + description: TextReference, + row: TransferUM.Row?, + isLoading: Boolean, +) { + if (isLoading || row?.isLoading == true) { + TokenActionRow( + iconRes = iconRes, + title = title, + description = description, + tailContent = { TailLoader() }, + ) + } else { + TokenActionRow( + iconRes = iconRes, + title = title, + description = description, + onClick = row?.onClick, + isEnabled = row?.isEnabled == true, + ) + } +} + +@Composable +private fun TailLoader() { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = TangemTheme.colors2.graphic.neutral.tertiary, + strokeWidth = 2.dp, + ) +} + +// region Preview +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview(@PreviewParameter(TransferPreviewProvider::class) state: TransferUM) { + TangemThemePreviewRedesign { + CompositionLocalProvider(LocalRedesignEnabled provides true) { + TransferBottomSheetContent( + state = state, + onCloseClick = {}, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} + +private class TransferPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + TransferUM.Loading, + TransferUM.Content( + send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swap = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + sell = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + ), + TransferUM.Content( + send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swap = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}), + sell = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}), + ), + TransferUM.Content( + send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swap = null, + sell = null, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 79dce61d97..4d34a77830 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -65,11 +66,28 @@ internal fun TokenDetailsBalanceBlock(balanceBlockUM: TokenDetailsBalanceBlockUM is TokenDetailsBalanceBlockUM.Loading -> LoadingBody() is TokenDetailsBalanceBlockUM.Error -> ErrorBody() } - SpacerH(TangemTheme.dimens2.x10) - ActionButtons(buttons = balanceBlockUM.actionButtons) + if (!balanceBlockUM.isBalanceZeroContent()) { + SpacerH(TangemTheme.dimens2.x10) + val buttons = remember( + balanceBlockUM.addFundsButton, + balanceBlockUM.swapButton, + balanceBlockUM.transferButton, + ) { + persistentListOf( + balanceBlockUM.addFundsButton, + balanceBlockUM.swapButton, + balanceBlockUM.transferButton, + ) + } + ActionButtons(buttons = buttons) + } } } +private fun TokenDetailsBalanceBlockUM.isBalanceZeroContent(): Boolean { + return (this as? TokenDetailsBalanceBlockUM.Content)?.isBalanceZero == true +} + @Composable private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content) { AnimatedContent( @@ -169,33 +187,45 @@ private fun TokenDetailsBalanceBlock_Preview( private class PreviewProvider : PreviewParameterProvider { - private val previewActionButtons = persistentListOf( - TangemButtonUM( - text = stringReference("Add funds"), - tangemIconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_arrow_down_24, - tintReference = { TangemTheme.colors2.graphic.neutral.primary }, - ), - onClick = { }, - isEnabled = true, - type = TangemButtonType.Secondary, + private val previewAddFundsButton = TangemButtonUM( + text = stringReference("Add funds"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_arrow_down_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, ), - TangemButtonUM( - text = stringReference("Transfer"), - tangemIconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_arrow_up_24, - tintReference = { TangemTheme.colors2.graphic.neutral.primary }, - ), - onClick = { }, - isEnabled = true, - type = TangemButtonType.Secondary, + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, + ) + + private val previewSwapButton = TangemButtonUM( + text = stringReference("Swap"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_exchange_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, ), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, + ) + + private val previewTransferButton = TangemButtonUM( + text = stringReference("Transfer"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_arrow_up_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, ) override val values: Sequence get() = sequenceOf( TokenDetailsBalanceBlockUM.Content( - actionButtons = previewActionButtons, + addFundsButton = previewAddFundsButton, + swapButton = previewSwapButton, + transferButton = previewTransferButton, tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( type = TokenBalanceTypeUM.Type.ALL, availableTypes = persistentListOf( @@ -210,9 +240,12 @@ private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ZeroBalanceActionsUM.Loading, + ZeroBalanceActionsUM.Content( + buy = ZeroBalanceActionsUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swap = ZeroBalanceActionsUM.Row(isLoading = false, isEnabled = false, onClick = {}), + receive = ZeroBalanceActionsUM.Row(isLoading = false, isEnabled = true, onClick = {}, onLongClick = {}), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformerTest.kt new file mode 100644 index 0000000000..f975184ef3 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindAddFundsActionButtonTransformerTest.kt @@ -0,0 +1,138 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class BindAddFundsActionButtonTransformerTest { + + private val onClick: () -> Unit = mockk(relaxed = true) + private val onLongClick: () -> Unit = mockk(relaxed = true) + private val previousAddFundsClick: () -> Unit = mockk(relaxed = true) + private val swapClick: () -> Unit = mockk(relaxed = true) + private val transferClick: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN buttons WHEN transform THEN onClick of add-funds button is replaced`() { + // GIVEN + val transformer = BindAddFundsActionButtonTransformer(onClick = onClick, onLongClick = onLongClick) + + // WHEN + val result = transformer.transform(stateWithButtons()) + result.balanceBlockUM.addFundsButton.onClick() + + // THEN + verify(exactly = 1) { onClick.invoke() } + verify(exactly = 0) { previousAddFundsClick.invoke() } + } + + @Test + fun `GIVEN buttons WHEN long-click invoked on Add funds THEN onLongClick fires`() { + // GIVEN + val transformer = BindAddFundsActionButtonTransformer(onClick = onClick, onLongClick = onLongClick) + + // WHEN + val result = transformer.transform(stateWithButtons()) + result.balanceBlockUM.addFundsButton.onLongClick!!() + + // THEN + verify(exactly = 1) { onLongClick.invoke() } + verify(exactly = 0) { onClick.invoke() } + } + + @Test + fun `GIVEN buttons WHEN transform THEN Swap and Transfer onClick are untouched`() { + // GIVEN + val transformer = BindAddFundsActionButtonTransformer(onClick = onClick, onLongClick = onLongClick) + + // WHEN + val result = transformer.transform(stateWithButtons()) + result.balanceBlockUM.swapButton.onClick() + result.balanceBlockUM.transferButton.onClick() + + // THEN + verify(exactly = 0) { onClick.invoke() } + verify(exactly = 1) { swapClick.invoke() } + verify(exactly = 1) { transferClick.invoke() } + } + + @Test + fun `GIVEN add-funds button WHEN transform THEN other fields of the button are preserved`() { + // GIVEN + val transformer = BindAddFundsActionButtonTransformer(onClick = onClick, onLongClick = onLongClick) + val state = stateWithButtons() + + // WHEN + val result = transformer.transform(state) + + // THEN + val original = state.balanceBlockUM.addFundsButton + val updated = result.balanceBlockUM.addFundsButton + assertThat(updated.text).isEqualTo(original.text) + assertThat(updated.tangemIconUM).isEqualTo(original.tangemIconUM) + assertThat(updated.type).isEqualTo(original.type) + assertThat(updated.isEnabled).isEqualTo(original.isEnabled) + } + + @Test + fun `GIVEN buttons WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + val transformer = BindAddFundsActionButtonTransformer(onClick = onClick, onLongClick = onLongClick) + val state = stateWithButtons() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + assertThat(result.addFundsUM).isSameInstanceAs(state.addFundsUM) + assertThat(result.transferUM).isSameInstanceAs(state.transferUM) + } + + private fun stateWithButtons(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("USDT"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + addFundsButton = button(text = "Add funds", onClick = previousAddFundsClick), + swapButton = button(text = "Swap", onClick = swapClick), + transferButton = button(text = "Transfer", onClick = transferClick), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun button(text: String, onClick: () -> Unit) = TangemButtonUM( + text = stringReference(text), + type = TangemButtonType.Secondary, + onClick = onClick, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformerTest.kt new file mode 100644 index 0000000000..1981fbfefb --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/BindTransferActionButtonTransformerTest.kt @@ -0,0 +1,123 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class BindTransferActionButtonTransformerTest { + + private val onClick: () -> Unit = mockk(relaxed = true) + private val addFundsClick: () -> Unit = mockk(relaxed = true) + private val swapClick: () -> Unit = mockk(relaxed = true) + private val previousTransferClick: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN buttons WHEN transform THEN onClick of transfer button is replaced`() { + // GIVEN + val transformer = BindTransferActionButtonTransformer(onClick = onClick) + + // WHEN + val result = transformer.transform(stateWithButtons()) + result.balanceBlockUM.transferButton.onClick() + + // THEN + verify(exactly = 1) { onClick.invoke() } + verify(exactly = 0) { previousTransferClick.invoke() } + } + + @Test + fun `GIVEN buttons WHEN transform THEN AddFunds and Swap onClick are untouched`() { + // GIVEN + val transformer = BindTransferActionButtonTransformer(onClick = onClick) + + // WHEN + val result = transformer.transform(stateWithButtons()) + result.balanceBlockUM.addFundsButton.onClick() + result.balanceBlockUM.swapButton.onClick() + + // THEN + verify(exactly = 0) { onClick.invoke() } + verify(exactly = 1) { addFundsClick.invoke() } + verify(exactly = 1) { swapClick.invoke() } + } + + @Test + fun `GIVEN transfer button WHEN transform THEN other fields of the button are preserved`() { + // GIVEN + val transformer = BindTransferActionButtonTransformer(onClick = onClick) + val state = stateWithButtons() + + // WHEN + val result = transformer.transform(state) + + // THEN + val original = state.balanceBlockUM.transferButton + val updated = result.balanceBlockUM.transferButton + assertThat(updated.text).isEqualTo(original.text) + assertThat(updated.tangemIconUM).isEqualTo(original.tangemIconUM) + assertThat(updated.type).isEqualTo(original.type) + assertThat(updated.isEnabled).isEqualTo(original.isEnabled) + } + + @Test + fun `GIVEN buttons WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + val transformer = BindTransferActionButtonTransformer(onClick = onClick) + val state = stateWithButtons() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + assertThat(result.addFundsUM).isSameInstanceAs(state.addFundsUM) + assertThat(result.transferUM).isSameInstanceAs(state.transferUM) + } + + private fun stateWithButtons(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("USDT"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + addFundsButton = button(text = "Add funds", onClick = addFundsClick), + swapButton = button(text = "Swap", onClick = swapClick), + transferButton = button(text = "Transfer", onClick = previousTransferClick), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun button(text: String, onClick: () -> Unit) = TangemButtonUM( + text = stringReference(text), + type = TangemButtonType.Secondary, + onClick = onClick, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt index 0fbcf212e3..9852c36388 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt @@ -3,13 +3,18 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.transfor import com.google.common.truth.Truth.assertThat import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -106,7 +111,9 @@ class InitializeWithCryptoCurrencyTransformerTest { // THEN — only top bar title/subtitle/onBackClick, marketPriceBlockState and pullToRefresh.onRefresh are touched assertThat(result.topAppBarUM.menuItems).isEqualTo(state.topAppBarUM.menuItems) - assertThat(result.balanceBlockUM.actionButtons).isEqualTo(state.balanceBlockUM.actionButtons) + assertThat(result.balanceBlockUM.addFundsButton).isEqualTo(state.balanceBlockUM.addFundsButton) + assertThat(result.balanceBlockUM.swapButton).isEqualTo(state.balanceBlockUM.swapButton) + assertThat(result.balanceBlockUM.transferButton).isEqualTo(state.balanceBlockUM.transferButton) assertThat(result.balanceBlockUM.tokenBalanceTypeUM).isEqualTo(state.balanceBlockUM.tokenBalanceTypeUM) assertThat(result.earnBlockState).isEqualTo(state.earnBlockState) assertThat(result.pullToRefreshConfig.isRefreshing).isEqualTo(state.pullToRefreshConfig.isRefreshing) @@ -139,7 +146,9 @@ class InitializeWithCryptoCurrencyTransformerTest { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = mockk(relaxed = true), ), @@ -149,6 +158,15 @@ class InitializeWithCryptoCurrencyTransformerTest { pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun placeholderButton(): TangemButtonUM = TangemButtonUM( + text = stringReference(""), + type = TangemButtonType.Secondary, + onClick = {}, ) private companion object { diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt index 8e8a26f034..2147f6b03b 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt @@ -7,13 +7,15 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.ds.button.TangemButtonType import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.mockk -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import org.junit.jupiter.api.Test @@ -48,22 +50,23 @@ class SetBalanceLoadingTransformerTest { @Test fun `GIVEN state with action buttons WHEN transform THEN action buttons are preserved`() { // GIVEN - val buttons = persistentListOf( - TangemButtonUM( - text = stringReference("Test"), - onClick = {}, - isEnabled = true, - type = TangemButtonType.Secondary, - ), + val addFunds = button(text = "Add funds") + val swap = button(text = "Swap") + val transfer = button(text = "Transfer") + val state = initialState( + addFundsButton = addFunds, + swapButton = swap, + transferButton = transfer, ) - val state = initialState(actionButtons = buttons) val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) // WHEN val result = transformer.transform(state) // THEN - assertThat(result.balanceBlockUM.actionButtons).isEqualTo(buttons) + assertThat(result.balanceBlockUM.addFundsButton).isSameInstanceAs(addFunds) + assertThat(result.balanceBlockUM.swapButton).isSameInstanceAs(swap) + assertThat(result.balanceBlockUM.transferButton).isSameInstanceAs(transfer) } @Test @@ -83,7 +86,9 @@ class SetBalanceLoadingTransformerTest { // GIVEN val contentState = initialState().copy( balanceBlockUM = TokenDetailsBalanceBlockUM.Content( - actionButtons = persistentListOf(), + addFundsButton = button(text = "Add funds"), + swapButton = button(text = "Swap"), + transferButton = button(text = "Transfer"), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, displayCryptoBalanceAll = stringReference("1.0 BTC"), @@ -91,6 +96,7 @@ class SetBalanceLoadingTransformerTest { displayCryptoBalanceAvailable = null, displayFiatBalanceAvailable = null, isBalanceFlickering = false, + isBalanceZero = false, ), ) val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) @@ -121,7 +127,9 @@ class SetBalanceLoadingTransformerTest { } private fun initialState( - actionButtons: ImmutableList = persistentListOf(), + addFundsButton: TangemButtonUM = button(text = "Add funds"), + swapButton: TangemButtonUM = button(text = "Swap"), + transferButton: TangemButtonUM = button(text = "Transfer"), ): TokenDetailsUM = TokenDetailsUM( topAppBarUM = TokenDetailsTopAppBarUM( titleState = TitleState.Simple(tokenName = ""), @@ -130,7 +138,9 @@ class SetBalanceLoadingTransformerTest { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = actionButtons, + addFundsButton = addFundsButton, + swapButton = swapButton, + transferButton = transferButton, tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), @@ -140,5 +150,14 @@ class SetBalanceLoadingTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun button(text: String): TangemButtonUM = TangemButtonUM( + text = stringReference(text), + type = TangemButtonType.Secondary, + onClick = {}, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt index a78c3ed430..bba4ce1980 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt @@ -5,6 +5,8 @@ import com.tangem.common.getTotalWithRewardsStakingBalance import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource @@ -12,11 +14,14 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.staking.StakingBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -168,12 +173,15 @@ class SetBalanceTransformerTest { // GIVEN val status = createStatus(loadedValue()) val transformer = createTransformer(status) + val state = initialState() // WHEN - val result = transformer.transform(initialState()) + val result = transformer.transform(state) // THEN - assertThat(result.balanceBlockUM.actionButtons).isEqualTo(initialState().balanceBlockUM.actionButtons) + assertThat(result.balanceBlockUM.addFundsButton).isEqualTo(state.balanceBlockUM.addFundsButton) + assertThat(result.balanceBlockUM.swapButton).isEqualTo(state.balanceBlockUM.swapButton) + assertThat(result.balanceBlockUM.transferButton).isEqualTo(state.balanceBlockUM.transferButton) } @Test @@ -271,7 +279,9 @@ class SetBalanceTransformerTest { val transformer = createTransformer(status) val prevContent = TokenDetailsBalanceBlockUM.Content( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( type = TokenBalanceTypeUM.Type.AVAILABLE, availableTypes = persistentListOf(TokenBalanceTypeUM.Type.ALL, TokenBalanceTypeUM.Type.AVAILABLE), @@ -283,6 +293,7 @@ class SetBalanceTransformerTest { displayCryptoBalanceAvailable = null, displayFiatBalanceAvailable = null, isBalanceFlickering = false, + isBalanceZero = false, ) val state = initialState().copy(balanceBlockUM = prevContent) @@ -339,6 +350,54 @@ class SetBalanceTransformerTest { // endregion + // region isBalanceZero + + @Test + fun `GIVEN amount is zero WHEN transform THEN isBalanceZero is true`() { + // GIVEN + val status = createStatus(loadedValue(amount = BigDecimal.ZERO, stakingBalance = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.isBalanceZero).isTrue() + } + + @Test + fun `GIVEN non-zero amount WHEN transform THEN isBalanceZero is false`() { + // GIVEN + val status = createStatus(loadedValue(amount = BigDecimal("0.001"), stakingBalance = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.isBalanceZero).isFalse() + } + + @Test + fun `GIVEN zero amount but non-zero staking WHEN transform THEN isBalanceZero is false`() { + // GIVEN — staking balance counts towards "total" so amount+staking != 0 keeps the rich UI + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(amount = BigDecimal.ZERO, stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.isBalanceZero).isFalse() + } + + // endregion + // region No staking → available balances @Test @@ -468,7 +527,9 @@ class SetBalanceTransformerTest { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), @@ -478,5 +539,14 @@ class SetBalanceTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun placeholderButton(): TangemButtonUM = TangemButtonUM( + text = stringReference(""), + type = TangemButtonType.Secondary, + onClick = {}, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt index 2e1d75e21c..daf5144730 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt @@ -11,10 +11,13 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -201,5 +204,8 @@ class SetTopBarTitleTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt index 99be5d0fa3..e48624e206 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt @@ -4,12 +4,17 @@ import com.google.common.truth.Truth.assertThat import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import org.junit.jupiter.api.Test @@ -83,7 +88,9 @@ class ToggleBalanceTypeTransformerTest { // GIVEN val state = initialState().copy( balanceBlockUM = TokenDetailsBalanceBlockUM.Error( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), @@ -101,7 +108,9 @@ class ToggleBalanceTypeTransformerTest { // GIVEN val state = initialState().copy( balanceBlockUM = TokenDetailsBalanceBlockUM.Content( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, displayCryptoBalanceAll = stringReference("1.0 ETH"), @@ -109,6 +118,7 @@ class ToggleBalanceTypeTransformerTest { displayCryptoBalanceAvailable = null, displayFiatBalanceAvailable = null, isBalanceFlickering = false, + isBalanceZero = false, ), ) @@ -151,7 +161,9 @@ class ToggleBalanceTypeTransformerTest { // THEN val resultContent = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content - assertThat(resultContent.actionButtons).isEqualTo(originalContent.actionButtons) + assertThat(resultContent.addFundsButton).isEqualTo(originalContent.addFundsButton) + assertThat(resultContent.swapButton).isEqualTo(originalContent.swapButton) + assertThat(resultContent.transferButton).isEqualTo(originalContent.transferButton) assertThat(resultContent.currencyIconState).isEqualTo(originalContent.currencyIconState) assertThat(resultContent.displayCryptoBalanceAll).isEqualTo(originalContent.displayCryptoBalanceAll) assertThat(resultContent.displayFiatBalanceAll).isEqualTo(originalContent.displayFiatBalanceAll) @@ -163,7 +175,9 @@ class ToggleBalanceTypeTransformerTest { private fun stateWithContent(type: TokenBalanceTypeUM.Type): TokenDetailsUM { return initialState().copy( balanceBlockUM = TokenDetailsBalanceBlockUM.Content( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( type = type, availableTypes = persistentListOf( @@ -178,6 +192,7 @@ class ToggleBalanceTypeTransformerTest { displayCryptoBalanceAvailable = stringReference("9.0 ETH"), displayFiatBalanceAvailable = stringReference("$18,000"), isBalanceFlickering = false, + isBalanceZero = false, ), ) } @@ -190,7 +205,9 @@ class ToggleBalanceTypeTransformerTest { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), + addFundsButton = placeholderButton(), + swapButton = placeholderButton(), + transferButton = placeholderButton(), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), @@ -200,5 +217,14 @@ class ToggleBalanceTypeTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) + + private fun placeholderButton(): TangemButtonUM = TangemButtonUM( + text = stringReference(""), + type = TangemButtonType.Secondary, + onClick = {}, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformerTest.kt new file mode 100644 index 0000000000..3e4eb0e634 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateAddFundsTransformerTest.kt @@ -0,0 +1,288 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class UpdateAddFundsTransformerTest { + + private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) + private val onActionDispatched: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN actions without Buy, Swap nor Receive WHEN transform THEN state is unchanged`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None)), + ) + val state = initialState() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + assertThat(result.addFundsUM).isInstanceOf(AddFundsUM.Loading::class.java) + } + + @Test + fun `GIVEN both Buy and Receive available WHEN transform THEN Content carries both rows`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.addFundsUM as AddFundsUM.Content + assertThat(content.buy).isNotNull() + assertThat(content.receive).isNotNull() + } + + @Test + fun `GIVEN Buy disabled AND Receive available WHEN transform THEN Buy row stays visible but disabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable("USDT")), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.addFundsUM as AddFundsUM.Content + assertThat(content.buy).isNotNull() + assertThat(content.buy?.isEnabled).isFalse() + assertThat(content.receive?.isEnabled).isTrue() + } + + @Test + fun `GIVEN only Buy available WHEN transform THEN Receive row is null`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.addFundsUM as AddFundsUM.Content + assertThat(content.buy).isNotNull() + assertThat(content.receive).isNull() + } + + @Test + fun `GIVEN Buy row WHEN onClick invoked THEN dispatcher fires before buy click`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).addFundsUM as AddFundsUM.Content + content.buy!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onBuyClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN Receive row WHEN long-clicked THEN dispatcher fires before copy address`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).addFundsUM as AddFundsUM.Content + content.receive!!.onLongClick!!() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onCopyAddress() + } + } + + @Test + fun `GIVEN Receive row WHEN onClick invoked THEN dispatcher fires before receive click`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).addFundsUM as AddFundsUM.Content + content.receive!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onReceiveClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN actions WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.transferUM).isSameInstanceAs(state.transferUM) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + } + + @Test + fun `GIVEN both Buy and Receive disabled WHEN transform THEN Content shows both rows disabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable("USDT")), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.UnassociatedAsset), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.addFundsUM as AddFundsUM.Content + assertThat(content.buy?.isEnabled).isFalse() + assertThat(content.receive?.isEnabled).isFalse() + } + + @Test + fun `GIVEN disabled Buy WHEN onClick invoked THEN buy click receives the unavailability reason`() { + // GIVEN — Row.onClick is always wired; UI gating decides whether it fires. This test + // guards the wiring: when the row IS invoked, the reason is forwarded. + val reason = ScenarioUnavailabilityReason.BuyUnavailable("USDT") + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(reason)), + ) + + // WHEN + val content = transformer.transform(initialState()).addFundsUM as AddFundsUM.Content + content.buy!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onBuyClick(reason) + } + } + + @Test + fun `GIVEN action with loading marker reason WHEN transform THEN that row is marked isLoading`() { + // GIVEN — DataLoading/ExpressLoading signal the underlying data is still being fetched. + // The row stays in Content but with isLoading=true so the UI keeps the spinner. + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.ExpressLoading("USDT")), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.addFundsUM as AddFundsUM.Content + assertThat(content.buy?.isLoading).isTrue() + assertThat(content.buy?.isEnabled).isFalse() + assertThat(content.receive?.isLoading).isFalse() + assertThat(content.receive?.isEnabled).isTrue() + } + + @Test + fun `GIVEN Swap row WHEN onClick invoked THEN swap-to click receives the reason`() { + // GIVEN — AddFunds context implies "swap something INTO this token", direction = TO. + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)), + ) + + // WHEN + val content = transformer.transform(initialState()).addFundsUM as AddFundsUM.Content + content.swap!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSwapToClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN row WHEN not clicked THEN no callbacks fire`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + transformer.transform(initialState()) + + // THEN + verify(exactly = 0) { onActionDispatched.invoke() } + verify(exactly = 0) { clickIntents.onBuyClick(any()) } + } + + private fun createTransformer(actions: List) = UpdateAddFundsTransformer( + actions = actions, + clickIntents = clickIntents, + onActionDispatched = onActionDispatched, + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("USDT"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateNotificationsTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateNotificationsTransformerTest.kt index add72c4956..19d4cea5ca 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateNotificationsTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateNotificationsTransformerTest.kt @@ -11,10 +11,13 @@ import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.model.warnings.KaspaWarnings import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.mockk import io.mockk.verify import kotlinx.collections.immutable.persistentListOf @@ -568,5 +571,8 @@ class UpdateNotificationsTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt index cc0a370391..31b1d632f2 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt @@ -14,10 +14,13 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.StakingOption import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -133,5 +136,8 @@ class UpdateStakingNotificationTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt index f12747803f..d66631ccae 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt @@ -7,10 +7,13 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -211,5 +214,8 @@ class UpdateTopBarMenuTransformerTest { pullToRefreshConfig = mockk(relaxed = true), isBalanceHidden = false, isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt new file mode 100644 index 0000000000..ebcaa0fb16 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt @@ -0,0 +1,271 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class UpdateTransferTransformerTest { + + private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) + private val onActionDispatched: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN actions without Send nor Sell WHEN transform THEN state is unchanged`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + val state = initialState() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + assertThat(result.transferUM).isInstanceOf(TransferUM.Loading::class.java) + } + + @Test + fun `GIVEN both Send and Sell available WHEN transform THEN Content carries both rows`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.transferUM as TransferUM.Content + assertThat(content.send).isNotNull() + assertThat(content.sell).isNotNull() + } + + @Test + fun `GIVEN Send disabled AND Sell available WHEN transform THEN Send row stays visible but disabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.UsedOutdatedData), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.transferUM as TransferUM.Content + assertThat(content.send?.isEnabled).isFalse() + assertThat(content.sell?.isEnabled).isTrue() + } + + @Test + fun `GIVEN Sell disabled AND Send available WHEN transform THEN Sell row stays visible but disabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.NotSupportedBySellService("USDT")), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.transferUM as TransferUM.Content + assertThat(content.send?.isEnabled).isTrue() + assertThat(content.sell?.isEnabled).isFalse() + } + + @Test + fun `GIVEN Send row WHEN onClick invoked THEN dispatcher fires before send click`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).transferUM as TransferUM.Content + content.send!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSendClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN Sell row WHEN onClick invoked THEN dispatcher fires before sell click`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).transferUM as TransferUM.Content + content.sell!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSellClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN actions WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.addFundsUM).isSameInstanceAs(state.addFundsUM) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + } + + @Test + fun `GIVEN both Send and Sell disabled WHEN transform THEN Content shows both rows disabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.UsedOutdatedData), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.transferUM as TransferUM.Content + assertThat(content.send?.isEnabled).isFalse() + assertThat(content.sell?.isEnabled).isFalse() + } + + @Test + fun `GIVEN disabled Send WHEN onClick invoked THEN send click receives the unavailability reason`() { + // GIVEN + val reason = ScenarioUnavailabilityReason.UsedOutdatedData + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(reason)), + ) + + // WHEN + val content = transformer.transform(initialState()).transferUM as TransferUM.Content + content.send!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSendClick(reason) + } + } + + @Test + fun `GIVEN action with loading marker reason WHEN transform THEN that row is marked isLoading`() { + // GIVEN — see UpdateAddFundsTransformerTest for rationale. Send/Sell don't normally + // receive these markers in production, but the row UM honours them uniformly anyway. + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.DataLoading), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.transferUM as TransferUM.Content + assertThat(content.send?.isLoading).isTrue() + assertThat(content.send?.isEnabled).isFalse() + assertThat(content.sell?.isLoading).isFalse() + assertThat(content.sell?.isEnabled).isTrue() + } + + @Test + fun `GIVEN Swap row WHEN onClick invoked THEN swap-from click receives the reason`() { + // GIVEN — Transfer context implies "swap THIS token to another", direction = FROM. + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)), + ) + + // WHEN + val content = transformer.transform(initialState()).transferUM as TransferUM.Content + content.swap!!.onClick() + + // THEN + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSwapFromClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN row WHEN not clicked THEN no callbacks fire`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + transformer.transform(initialState()) + + // THEN + verify(exactly = 0) { onActionDispatched.invoke() } + verify(exactly = 0) { clickIntents.onSendClick(any()) } + } + + private fun createTransformer(actions: List) = UpdateTransferTransformer( + actions = actions, + clickIntents = clickIntents, + onActionDispatched = onActionDispatched, + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("USDT"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt new file mode 100644 index 0000000000..087a3246a7 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt @@ -0,0 +1,231 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class UpdateZeroBalanceActionsTransformerTest { + + private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) + + @Test + fun `GIVEN actions without Buy Swap or Receive WHEN transform THEN state is unchanged`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None)), + ) + val state = initialState() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + assertThat(result.zeroBalanceActionsUM).isInstanceOf(ZeroBalanceActionsUM.Loading::class.java) + } + + @Test + fun `GIVEN all three actions available WHEN transform THEN Content carries all rows enabled`() { + // GIVEN + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + assertThat(content.buy?.isEnabled).isTrue() + assertThat(content.swap?.isEnabled).isTrue() + assertThat(content.receive?.isEnabled).isTrue() + } + + @Test + fun `GIVEN Swap with unavailability reason WHEN transform THEN Swap row stays visible but disabled`() { + // GIVEN — when Swap carries any non-None unavailability reason the row must stay visible + // (layout keeps three slots) but render disabled; click is gated at the row level + // via isEnabled = false. A None reason still produces an enabled row (see test above). + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.UsedOutdatedData, false), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + assertThat(content.swap).isNotNull() + assertThat(content.swap?.isEnabled).isFalse() + assertThat(content.buy?.isEnabled).isTrue() + assertThat(content.receive?.isEnabled).isTrue() + } + + @Test + fun `GIVEN missing actions WHEN transform THEN absent rows are null`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + assertThat(content.buy).isNotNull() + assertThat(content.swap).isNull() + assertThat(content.receive).isNull() + } + + @Test + fun `GIVEN Buy row WHEN onClick invoked THEN buy click is dispatched with reason`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + content.buy!!.onClick() + + // THEN + verify(exactly = 1) { clickIntents.onBuyClick(ScenarioUnavailabilityReason.None) } + } + + @Test + fun `GIVEN disabled Swap WHEN onClick invoked THEN swap click receives the unavailability reason`() { + // GIVEN — Row.onClick is always wired; UI gating (isEnabled=false) decides whether it fires. + // This test guards the wiring: when the row IS invoked, the reason is forwarded so callers + // could decide to show the unavailability dialog if they ever drop the UI gating. + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.UsedOutdatedData, false), + ), + ) + + // WHEN + val content = transformer.transform(initialState()).zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + content.swap!!.onClick() + + // THEN + verify(exactly = 1) { clickIntents.onSwapToClick(ScenarioUnavailabilityReason.UsedOutdatedData) } + } + + @Test + fun `GIVEN Receive row WHEN long-clicked THEN copy address is dispatched`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val content = transformer.transform(initialState()).zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + content.receive!!.onLongClick!!() + + // THEN + verify(exactly = 1) { clickIntents.onCopyAddress() } + } + + @Test + fun `GIVEN actions WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.addFundsUM).isSameInstanceAs(state.addFundsUM) + assertThat(result.transferUM).isSameInstanceAs(state.transferUM) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + } + + @Test + fun `GIVEN action with loading marker reason WHEN transform THEN that row is marked isLoading`() { + // GIVEN — Swap commonly arrives with DataLoading while networkSource is still CACHE. + // The Swap row stays in Content but with isLoading=true so the UI keeps the spinner. + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.DataLoading, false), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.zeroBalanceActionsUM as ZeroBalanceActionsUM.Content + assertThat(content.swap?.isLoading).isTrue() + assertThat(content.swap?.isEnabled).isFalse() + assertThat(content.buy?.isLoading).isFalse() + assertThat(content.receive?.isLoading).isFalse() + } + + @Test + fun `GIVEN row WHEN not clicked THEN no callbacks fire`() { + // GIVEN + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)), + ) + + // WHEN + transformer.transform(initialState()) + + // THEN + verify(exactly = 0) { clickIntents.onBuyClick(any()) } + } + + private fun createTransformer(actions: List) = UpdateZeroBalanceActionsTransformer( + actions = actions, + clickIntents = clickIntents, + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("USDT"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + notifications = persistentListOf(), + marketPriceBlockState = mockk(relaxed = true), + earnBlockState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + addFundsUM = AddFundsUM.Loading, + transferUM = TransferUM.Loading, + zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading, + ) +} \ No newline at end of file