Updated on 2026-08-14
This commit is contained in:
parent
ad9def0d21
commit
a9b5490cc4
46 changed files with 2679 additions and 163 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ fun ActionButtons(buttons: ImmutableList<TangemButtonUM>, modifier: Modifier = M
|
|||
onClick = button.onClick,
|
||||
isEnabled = button.isEnabled,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
onLongClick = button.onLongClick,
|
||||
)
|
||||
Text(
|
||||
text = button.text.orEmpty().resolveReference(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -61,4 +61,7 @@ sealed class ScenarioUnavailabilityReason {
|
|||
enum class WithdrawalScenario {
|
||||
SELL, SEND // TODO staking create&process STAKING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ScenarioUnavailabilityReason.isLoading: Boolean
|
||||
get() = this == ScenarioUnavailabilityReason.DataLoading || this is ScenarioUnavailabilityReason.ExpressLoading
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */ }
|
||||
|
|
|
|||
|
|
@ -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<TokenDetailsUM> get() = redesignStateController.uiState
|
||||
|
||||
val addFundsUiState: StateFlow<AddFundsUM>
|
||||
field = redesignStateController.uiState
|
||||
.map { it.addFundsUM }
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = redesignStateController.value.addFundsUM,
|
||||
)
|
||||
|
||||
val transferUiState: StateFlow<TransferUM>
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -30,4 +30,10 @@ sealed class TokenDetailsBottomSheetConfig : Route {
|
|||
|
||||
@Serializable
|
||||
data object DynamicAddresses : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object AddFunds : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object Transfer : TokenDetailsBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,18 +11,24 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
@Immutable
|
||||
internal sealed class TokenDetailsBalanceBlockUM {
|
||||
|
||||
abstract val actionButtons: ImmutableList<TangemButtonUM>
|
||||
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<TangemButtonUM>,
|
||||
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<TangemButtonUM>,
|
||||
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<TangemButtonUM>,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TokenActionsState.ActionState>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
private val onActionDispatched: () -> Unit,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
private val onActionDispatched: () -> Unit,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ExpressTransactionsBlockState> = MutableStateFlow(
|
||||
ExpressTransactionsBlockState(
|
||||
|
|
|
|||
|
|
@ -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<AddFundsUM>,
|
||||
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<AddFundsUM>(
|
||||
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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddFundsUM> {
|
||||
override val values: Sequence<AddFundsUM> = 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
|
||||
|
|
@ -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<TransferUM>,
|
||||
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<TransferUM>(
|
||||
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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TransferUM> {
|
||||
override val values: Sequence<TransferUM> = 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
|
||||
|
|
@ -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<TokenDetailsBalanceBlockUM> {
|
||||
|
||||
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<TokenDetailsBalanceBlockUM>
|
||||
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<TokenDetailsBalanceBloc
|
|||
displayCryptoBalanceAvailable = stringReference("0.05 BTC"),
|
||||
displayFiatBalanceAvailable = stringReference("$10,000.00"),
|
||||
isBalanceFlickering = false,
|
||||
isBalanceZero = false,
|
||||
),
|
||||
TokenDetailsBalanceBlockUM.Content(
|
||||
actionButtons = previewActionButtons,
|
||||
addFundsButton = previewAddFundsButton,
|
||||
swapButton = previewSwapButton,
|
||||
transferButton = previewTransferButton,
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
displayCryptoBalanceAll = stringReference("123.456 USDT"),
|
||||
|
|
@ -220,14 +253,32 @@ private class PreviewProvider : PreviewParameterProvider<TokenDetailsBalanceBloc
|
|||
displayCryptoBalanceAvailable = null,
|
||||
displayFiatBalanceAvailable = null,
|
||||
isBalanceFlickering = false,
|
||||
isBalanceZero = false,
|
||||
),
|
||||
TokenDetailsBalanceBlockUM.Content(
|
||||
addFundsButton = previewAddFundsButton,
|
||||
swapButton = previewSwapButton,
|
||||
transferButton = previewTransferButton,
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
displayCryptoBalanceAll = stringReference("0 USDT"),
|
||||
displayFiatBalanceAll = stringReference("$0.00"),
|
||||
displayCryptoBalanceAvailable = null,
|
||||
displayFiatBalanceAvailable = null,
|
||||
isBalanceFlickering = false,
|
||||
isBalanceZero = true,
|
||||
),
|
||||
TokenDetailsBalanceBlockUM.Loading(
|
||||
actionButtons = previewActionButtons,
|
||||
addFundsButton = previewAddFundsButton,
|
||||
swapButton = previewSwapButton,
|
||||
transferButton = previewTransferButton,
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
),
|
||||
TokenDetailsBalanceBlockUM.Error(
|
||||
actionButtons = previewActionButtons,
|
||||
addFundsButton = previewAddFundsButton,
|
||||
swapButton = previewSwapButton,
|
||||
transferButton = previewTransferButton,
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
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.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.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
|
||||
@Composable
|
||||
internal fun ZeroBalanceActionsBlock(state: ZeroBalanceActionsUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens2.x10),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_credit_card_20,
|
||||
title = resourceReference(CoreR.string.common_buy),
|
||||
description = resourceReference(CoreR.string.quick_action_buy_description),
|
||||
row = (state as? ZeroBalanceActionsUM.Content)?.buy,
|
||||
isLoading = state is ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_exchange_mini_24,
|
||||
title = resourceReference(CoreR.string.common_swap),
|
||||
description = resourceReference(CoreR.string.quick_action_swap_description),
|
||||
row = (state as? ZeroBalanceActionsUM.Content)?.swap,
|
||||
isLoading = state is ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
ActionRow(
|
||||
iconRes = CoreR.drawable.ic_qrcode_new_24,
|
||||
title = resourceReference(CoreR.string.common_receive),
|
||||
description = resourceReference(CoreR.string.quick_action_receive_description),
|
||||
row = (state as? ZeroBalanceActionsUM.Content)?.receive,
|
||||
isLoading = state is ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
@DrawableRes iconRes: Int,
|
||||
title: TextReference,
|
||||
description: TextReference,
|
||||
row: ZeroBalanceActionsUM.Row?,
|
||||
isLoading: Boolean,
|
||||
) {
|
||||
// UM-level Loading (no emission yet) OR per-row Loading (Buy/Swap reason carries Loading
|
||||
// marker) → spinner. Otherwise chevron tail with `isEnabled` gating clicks.
|
||||
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(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(ZeroBalanceActionsPreviewProvider::class) state: ZeroBalanceActionsUM) {
|
||||
TangemThemePreviewRedesign {
|
||||
ZeroBalanceActionsBlock(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class ZeroBalanceActionsPreviewProvider : PreviewParameterProvider<ZeroBalanceActionsUM> {
|
||||
override val values: Sequence<ZeroBalanceActionsUM> = 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
|
||||
|
|
@ -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<MarketPriceBlockState>(relaxed = true),
|
||||
earnBlockState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MarketPriceBlockState>(relaxed = true),
|
||||
earnBlockState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<TangemButtonUM> = 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<PullToRefreshConfig>(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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>) = 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<TokenDetailsBalanceBlockUM>(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
earnBlockState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>) = 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<TokenDetailsBalanceBlockUM>(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
earnBlockState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>) = 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<TokenDetailsBalanceBlockUM>(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
earnBlockState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue