diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 586600272c..93d7812b80 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1707,6 +1707,8 @@ Selling %s is not supported by current providers, but we are working to add more options. Staking %s is not supported by current providers, but we are working to add more options. Approval has been revoked. Your funds remain in Yield mode. To perform actions, please go to Yield mode and grant permission again. + Available balance + Total balance Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index f5cb062474..c0e1f4704f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -42,10 +42,11 @@ fun CurrencyIcon( withFixedSize: Boolean = true, iconSize: Dp = 36.dp, ) { + val outerSize = iconSize + 4.dp Box( modifier = modifier .conditional(withFixedSize) { - size(size = 40.dp) + size(size = outerSize) }, ) { val iconModifier = Modifier diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index bb946cf2a8..2a3f0c895f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -32,6 +32,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.common.ui.tokens.getUnavailabilityReasonText +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference @@ -104,7 +105,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceLoadingTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer 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.UpdateTopBarMenuTransformer import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R @@ -176,6 +180,7 @@ internal class TokenDetailsModel @Inject constructor( private val getWalletIconUseCase: GetWalletIconUseCase, private val walletIconUMConverter: WalletIconUMConverter, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val designFeatureToggles: DesignFeatureToggles, private val redesignStateController: TokenDetailsStateController, ) : Model(), TokenDetailsClickIntents, @@ -197,6 +202,7 @@ internal class TokenDetailsModel @Inject constructor( private val stakingJobHolder = JobHolder() private val yieldSupplyBalanceJobHolder = JobHolder() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val redesignBalanceJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var account: Account.CryptoPortfolio? = null @@ -278,10 +284,8 @@ internal class TokenDetailsModel @Inject constructor( } init { - initRedesignState() + initRedesign() updateTopBarMenu() - updateRedesignTopBarMenu() - observeRedesignTopBarTitle() initButtons() updateContent() handleBalanceHiding() @@ -893,6 +897,11 @@ internal class TokenDetailsModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { uiState.value = stateFactory.getRefreshingState() + redesignStateController.update( + SetBalanceLoadingTransformer( + currencyIconState = redesignStateController.value.balanceBlockUM.currencyIconState, + ), + ) modelScope.launch(dispatchers.main) { listOf( @@ -1368,6 +1377,29 @@ internal class TokenDetailsModel @Inject constructor( // endregion Clore migration + private fun observeRedesignBalance() { + getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .map { it.status } + .distinctUntilChanged() + .combine(selectedAppCurrencyFlow) { status, appCurrency -> status to appCurrency } + .onEach { (status, appCurrency) -> + redesignStateController.update( + SetBalanceTransformer( + status = status, + appCurrency = appCurrency, + onToggleBalanceType = ::toggleRedesignBalanceType, + ), + ) + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + .saveIn(redesignBalanceJobHolder) + } + + private fun toggleRedesignBalanceType() { + redesignStateController.update(ToggleBalanceTypeTransformer()) + } + private fun updateRedesignTopBarMenu() { modelScope.launch(dispatchers.main) { val hasDerivations = networkHasDerivationUseCase( @@ -1389,6 +1421,14 @@ internal class TokenDetailsModel @Inject constructor( } } + private fun initRedesign() { + if (!designFeatureToggles.isRedesignEnabled) return + initRedesignState() + observeRedesignBalance() + updateRedesignTopBarMenu() + observeRedesignTopBarTitle() + } + private fun initRedesignState() { redesignStateController.update( InitializeWithCryptoCurrencyTransformer( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt index aab3bd7c14..a4cfef3e52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.ImmutableList @Immutable @@ -23,10 +25,25 @@ internal sealed class TokenDetailsBalanceBlockUM { override val actionButtons: ImmutableList, override val tokenBalanceTypeUM: TokenBalanceTypeUM, override val currencyIconState: CurrencyIconState, - val displayCryptoBalance: TextReference, - val displayFiatBalance: TextReference, + val displayCryptoBalanceAll: TextReference, + val displayFiatBalanceAll: TextReference, + val displayCryptoBalanceAvailable: TextReference?, + val displayFiatBalanceAvailable: TextReference?, val isBalanceFlickering: Boolean, - ) : TokenDetailsBalanceBlockUM() + ) : TokenDetailsBalanceBlockUM() { + + val displayCryptoBalance: TextReference + get() = when (tokenBalanceTypeUM.type) { + TokenBalanceTypeUM.Type.ALL -> displayCryptoBalanceAll + TokenBalanceTypeUM.Type.AVAILABLE -> displayCryptoBalanceAvailable ?: displayCryptoBalanceAll + } + + val displayFiatBalance: TextReference + get() = when (tokenBalanceTypeUM.type) { + TokenBalanceTypeUM.Type.ALL -> displayFiatBalanceAll + TokenBalanceTypeUM.Type.AVAILABLE -> displayFiatBalanceAvailable ?: displayFiatBalanceAll + } + } data class Error( override val actionButtons: ImmutableList, @@ -34,11 +51,11 @@ internal sealed class TokenDetailsBalanceBlockUM { override val currencyIconState: CurrencyIconState, ) : TokenDetailsBalanceBlockUM() - fun copyActionButtons(buttons: ImmutableList): TokenDetailsBalanceBlockUM { + fun copyCurrencyIconState(iconState: CurrencyIconState): TokenDetailsBalanceBlockUM { return when (this) { - is Content -> this.copy(actionButtons = buttons) - is Error -> this.copy(actionButtons = buttons) - is Loading -> this.copy(actionButtons = buttons) + is Content -> this.copy(currencyIconState = iconState) + is Error -> this.copy(currencyIconState = iconState) + is Loading -> this.copy(currencyIconState = iconState) } } } @@ -54,11 +71,11 @@ internal sealed class TokenBalanceTypeUM { data class Multiple( override val type: Type, val availableTypes: ImmutableList, - val onSelect: (Type) -> Unit, + val onSelect: () -> Unit, ) : TokenBalanceTypeUM() - enum class Type { - ALL, - AVAILABLE, + enum class Type(val text: TextReference) { + ALL(resourceReference(R.string.token_details_balance_total)), + AVAILABLE(resourceReference(R.string.token_details_balance_available)), } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt index 41e505d325..7fcf3fadd1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt @@ -4,7 +4,12 @@ import com.tangem.core.decompose.di.ModelScoped 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.ds.image.TangemIconUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -37,7 +42,22 @@ internal class TokenDetailsStateController @Inject constructor() { menuItems = persistentListOf(), ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), + 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, + ), + ), tokenBalanceTypeUM = TokenBalanceTypeUM.Single, currencyIconState = CurrencyIconState.Loading, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt index 64f054639c..3ecf879fe4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency @@ -12,12 +13,16 @@ internal class InitializeWithCryptoCurrencyTransformer( private val onBackClick: () -> Unit, ) : Transformer { - override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( - topAppBarUM = prevState.topAppBarUM.copy( - titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = cryptoCurrency.name), - subtitle = stringReference(cryptoCurrency.symbol), - onBackClick = onBackClick, - ), - marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), - ) + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val iconState = CryptoCurrencyToIconStateConverter().convert(cryptoCurrency) + return prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy( + titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = cryptoCurrency.name), + subtitle = stringReference(cryptoCurrency.symbol), + onBackClick = onBackClick, + ), + balanceBlockUM = prevState.balanceBlockUM.copyCurrencyIconState(iconState), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt new file mode 100644 index 0000000000..5b4852a7e3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +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.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class SetBalanceLoadingTransformer( + private val currencyIconState: CurrencyIconState, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val prevBalance = prevState.balanceBlockUM + return prevState.copy( + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = prevBalance.actionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = currencyIconState, + ), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt new file mode 100644 index 0000000000..28da2953eb --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.getTotalWithRewardsStakingBalance +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.defaultAmount +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.StakingBalance +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.TokenDetailsUM +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +/** + * Maps [CryptoCurrencyStatus] into [TokenDetailsBalanceBlockUM] and sets it on the state. + * + * Produces [TokenDetailsBalanceBlockUM.Content] for loaded states, + * [TokenDetailsBalanceBlockUM.Loading] for loading, + * [TokenDetailsBalanceBlockUM.Error] for unreachable/no-amount/missed-derivation. + */ +internal class SetBalanceTransformer( + private val status: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val onToggleBalanceType: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val prev = prevState.balanceBlockUM + val balanceBlockUM = when (status.value) { + is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockUM.Loading( + actionButtons = prev.actionButtons, + tokenBalanceTypeUM = prev.tokenBalanceTypeUM, + currencyIconState = prev.currencyIconState, + ) + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Custom, + -> buildLoadedContent(prev) + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> TokenDetailsBalanceBlockUM.Error( + actionButtons = prev.actionButtons, + tokenBalanceTypeUM = prev.tokenBalanceTypeUM, + currencyIconState = prev.currencyIconState, + ) + } + return prevState.copy(balanceBlockUM = balanceBlockUM) + } + + private fun buildLoadedContent(prev: TokenDetailsBalanceBlockUM): TokenDetailsBalanceBlockUM.Content { + val stakingCryptoAmount = + (status.value.stakingBalance as? StakingBalance.Data)?.getTotalWithRewardsStakingBalance( + status.currency.network.rawId, + ) + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + val hasStaking = !stakingCryptoAmount.isNullOrZero() + + val prevType = prev.tokenBalanceTypeUM + val tokenBalanceTypeUM = if (hasStaking) { + TokenBalanceTypeUM.Multiple( + type = (prevType as? TokenBalanceTypeUM.Multiple)?.type ?: TokenBalanceTypeUM.Type.ALL, + availableTypes = persistentListOf(TokenBalanceTypeUM.Type.ALL, TokenBalanceTypeUM.Type.AVAILABLE), + onSelect = onToggleBalanceType, + ) + } else { + TokenBalanceTypeUM.Single + } + + return TokenDetailsBalanceBlockUM.Content( + actionButtons = prev.actionButtons, + currencyIconState = prev.currencyIconState, + tokenBalanceTypeUM = tokenBalanceTypeUM, + displayFiatBalanceAll = formatFiatStyled( + fiatAmount = computeTotal(status.value.fiatAmount, stakingFiatAmount), + ), + displayCryptoBalanceAll = formatCrypto( + amount = computeTotal(status.value.amount, stakingCryptoAmount), + ), + displayFiatBalanceAvailable = if (hasStaking) { + formatFiatStyled(fiatAmount = status.value.fiatAmount) + } else { + null + }, + displayCryptoBalanceAvailable = if (hasStaking) { + formatCrypto(amount = status.value.amount) + } else { + null + }, + isBalanceFlickering = status.value.sources.total == StatusSource.CACHE, + ) + } + + private fun computeTotal(base: BigDecimal?, staking: BigDecimal?): BigDecimal? { + if (base == null) return null + return if (staking != null) base + staking else base + } + + private fun formatFiatStyled(fiatAmount: BigDecimal?): TextReference { + if (fiatAmount == null) return stringReference(DASH_SIGN) + return fiatAmount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ).defaultAmount( + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + } + } + + private fun formatCrypto(amount: BigDecimal?): TextReference { + if (amount == null) return stringReference(DASH_SIGN) + return stringReference( + amount.format { crypto(status.currency).defaultAmount() }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformer.kt new file mode 100644 index 0000000000..69302a474a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformer.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +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.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +/** + * Toggles the balance type between [TokenBalanceTypeUM.Type.ALL] and [TokenBalanceTypeUM.Type.AVAILABLE]. + * + * No-op if the current balance state is not [TokenDetailsBalanceBlockUM.Content] or its + * [TokenDetailsBalanceBlockUM.Content.tokenBalanceTypeUM] is not [TokenBalanceTypeUM.Multiple]. + */ +internal class ToggleBalanceTypeTransformer : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM { + val content = prevState.balanceBlockUM as? TokenDetailsBalanceBlockUM.Content ?: return prevState + val multiple = content.tokenBalanceTypeUM as? TokenBalanceTypeUM.Multiple ?: return prevState + + val nextType = when (multiple.type) { + TokenBalanceTypeUM.Type.ALL -> TokenBalanceTypeUM.Type.AVAILABLE + TokenBalanceTypeUM.Type.AVAILABLE -> TokenBalanceTypeUM.Type.ALL + } + + return prevState.copy( + balanceBlockUM = content.copy( + tokenBalanceTypeUM = multiple.copy(type = nextType), + ), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 6a1b96d329..25f0277b18 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -3,14 +3,20 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import android.content.res.Configuration import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn + import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll + +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.account.AccountIconUM @@ -33,6 +39,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta 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.ui.components.TokenDetailsBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockHeight import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.HazeTint import kotlinx.collections.immutable.persistentListOf @@ -41,10 +49,10 @@ import kotlinx.collections.immutable.persistentListOf internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifier = Modifier) { val topAppBarUM = tokenDetailsUM.topAppBarUM - // TODO [REDACTED_TASK_KEY] Token Details Make Balance with actions - val balanceBlockHeight = 200.dp - val partialCollapsedHeight = 0.dp - val expandedHeight = balanceBlockHeight + partialCollapsedHeight + val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } + val topBarHeight = 64.dp + val partialCollapsedHeight = topBarHeight + statusBarHeight + val expandedHeight = TokenDetailsBalanceBlockHeight + partialCollapsedHeight val behavior = rememberTangemExitUntilCollapsedScrollBehavior( expandedHeight = expandedHeight, @@ -62,11 +70,12 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi TangemCollapsingTopBar( state = behavior.state, collapsingPart = { - // [REDACTED_TASK_KEY] Token Details Make Balance with actions - Box( + TokenDetailsBalanceBlock( + balanceBlockUM = tokenDetailsUM.balanceBlockUM, modifier = Modifier .fillMaxWidth() - .height(balanceBlockHeight), + .statusBarsPadding() + .padding(top = topBarHeight), ) }, body = { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt new file mode 100644 index 0000000000..ee3f598efc --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -0,0 +1,239 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +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 androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.button.action.ActionButtons +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.LocalRootBackgroundColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.persistentListOf + +private val CurrencyIconSize: Dp = 70.dp +private val NetworkBadgeSize: Dp = 24.dp +internal val TokenDetailsBalanceBlockHeight: Dp = 404.dp + +@Composable +internal fun TokenDetailsBalanceBlock(balanceBlockUM: TokenDetailsBalanceBlockUM, modifier: Modifier = Modifier) { + val rootBackground by LocalRootBackgroundColor.current + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens2.x10), + ) { + CurrencyIcon( + state = balanceBlockUM.currencyIconState, + shouldDisplayNetwork = true, + iconSize = CurrencyIconSize, + networkBadgeSize = NetworkBadgeSize, + networkBadgeBackground = rootBackground, + ) + SpacerH(TangemTheme.dimens2.x3) + when (balanceBlockUM) { + is TokenDetailsBalanceBlockUM.Content -> ContentBody(state = balanceBlockUM) + is TokenDetailsBalanceBlockUM.Loading -> LoadingBody() + is TokenDetailsBalanceBlockUM.Error -> ErrorBody() + } + SpacerH(TangemTheme.dimens2.x10) + ActionButtons(buttons = balanceBlockUM.actionButtons) + } +} + +@Composable +private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content) { + AnimatedContent( + targetState = state.tokenBalanceTypeUM.type, + label = "Token balance type", + ) { currentType -> + val tokenBalanceTypeUM = state.tokenBalanceTypeUM + when (tokenBalanceTypeUM) { + is TokenBalanceTypeUM.Multiple -> Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + modifier = Modifier.clickable(onClick = tokenBalanceTypeUM.onSelect), + ) { + Text( + text = currentType.text.resolveReference(), + style = TangemTheme.typography2.calloutSemibold15, + color = TangemTheme.colors2.text.neutral.secondary, + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_sort_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.secondary, + modifier = Modifier.size(TangemTheme.dimens2.x4), + ) + } + TokenBalanceTypeUM.Single -> Text( + text = currentType.text.resolveReference(), + style = TangemTheme.typography2.calloutSemibold15, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } + } + SpacerH(TangemTheme.dimens2.x2) + Text( + text = state.displayFiatBalance.resolveAnnotatedReference(), + style = TangemTheme.typography2.titleRegular44, + color = TangemTheme.colors2.text.neutral.primary, + ) + SpacerH(TangemTheme.dimens2.x2_5) + Text( + text = state.displayCryptoBalance.resolveAnnotatedReference(), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.secondary, + ) +} + +@Composable +private fun LoadingBody() { + Text( + text = TokenBalanceTypeUM.Type.ALL.text.resolveReference(), + style = TangemTheme.typography2.calloutSemibold15, + color = TangemTheme.colors2.text.neutral.secondary, + ) + SpacerH(TangemTheme.dimens2.x2) + TextShimmer( + style = TangemTheme.typography2.titleRegular44, + text = "$1234567890", + radius = TangemTheme.dimens2.x6, + ) + SpacerH(TangemTheme.dimens2.x2) + TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + text = "12345.67", + radius = TangemTheme.dimens2.x4, + ) +} + +@Composable +private fun ErrorBody() { + Text( + text = "—", + style = TangemTheme.typography2.titleRegular44, + color = TangemTheme.colors2.text.neutral.primary, + ) + SpacerH(TangemTheme.dimens2.x2_5) + Text( + text = "—", + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.secondary, + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenDetailsBalanceBlock_Preview( + @PreviewParameter(PreviewProvider::class) params: TokenDetailsBalanceBlockUM, +) { + TangemThemePreviewRedesign { + TokenDetailsBalanceBlock( + balanceBlockUM = params, + modifier = Modifier.background(TangemTheme.colors2.surface.level2), + ) + } +} + +private class PreviewProvider : PreviewParameterProvider { + + private val previewActionButtons = persistentListOf( + TangemButtonUM( + text = stringReference("Add funds"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_arrow_down_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, + ), + TangemButtonUM( + text = stringReference("Transfer"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_arrow_up_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onClick = { }, + isEnabled = true, + type = TangemButtonType.Secondary, + ), + ) + + override val values: Sequence + get() = sequenceOf( + TokenDetailsBalanceBlockUM.Content( + actionButtons = previewActionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( + type = TokenBalanceTypeUM.Type.ALL, + availableTypes = persistentListOf( + TokenBalanceTypeUM.Type.ALL, + TokenBalanceTypeUM.Type.AVAILABLE, + ), + onSelect = { }, + ), + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("0.0613884 BTC"), + displayFiatBalanceAll = stringReference("$12,380.94"), + displayCryptoBalanceAvailable = stringReference("0.05 BTC"), + displayFiatBalanceAvailable = stringReference("$10,000.00"), + isBalanceFlickering = false, + ), + TokenDetailsBalanceBlockUM.Content( + actionButtons = previewActionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("123.456 USDT"), + displayFiatBalanceAll = stringReference("$123.45"), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + ), + TokenDetailsBalanceBlockUM.Loading( + actionButtons = previewActionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + TokenDetailsBalanceBlockUM.Error( + actionButtons = previewActionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt index 43672cf4c9..8c0623c3e8 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +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 @@ -99,7 +100,8 @@ class InitializeWithCryptoCurrencyTransformerTest { // THEN — only top bar title/subtitle/onBackClick and marketPriceBlockState are touched assertThat(result.topAppBarUM.menuItems).isEqualTo(state.topAppBarUM.menuItems) - assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.balanceBlockUM.actionButtons).isEqualTo(state.balanceBlockUM.actionButtons) + assertThat(result.balanceBlockUM.tokenBalanceTypeUM).isEqualTo(state.balanceBlockUM.tokenBalanceTypeUM) assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) @@ -113,7 +115,11 @@ class InitializeWithCryptoCurrencyTransformerTest { onBackClick = {}, menuItems = persistentListOf(), ), - balanceBlockUM = mockk(relaxed = true), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = mockk(relaxed = true), + ), marketPriceBlockState = mockk(relaxed = true), stakingBlocksState = null, pullToRefreshConfig = mockk(relaxed = true), diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt new file mode 100644 index 0000000000..1d7b67394f --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceLoadingTransformerTest.kt @@ -0,0 +1,143 @@ +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.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 io.mockk.mockk +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class SetBalanceLoadingTransformerTest { + + private val currencyIconState: CurrencyIconState = mockk(relaxed = true) + + @Test + fun `GIVEN any state WHEN transform THEN balance block is Loading`() { + // GIVEN + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Loading::class.java) + } + + @Test + fun `GIVEN currency icon state WHEN transform THEN Loading block carries that icon state`() { + // GIVEN + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM.currencyIconState).isSameInstanceAs(currencyIconState) + } + + @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 state = initialState(actionButtons = buttons) + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.balanceBlockUM.actionButtons).isEqualTo(buttons) + } + + @Test + fun `GIVEN any state WHEN transform THEN balance type is Single`() { + // GIVEN + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM.tokenBalanceTypeUM).isEqualTo(TokenBalanceTypeUM.Single) + } + + @Test + fun `GIVEN Content balance block WHEN transform THEN switches to Loading`() { + // GIVEN + val contentState = initialState().copy( + balanceBlockUM = TokenDetailsBalanceBlockUM.Content( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("1.0 BTC"), + displayFiatBalanceAll = stringReference("$50,000"), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + ), + ) + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(contentState) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Loading::class.java) + } + + @Test + fun `GIVEN any state WHEN transform THEN unrelated fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = SetBalanceLoadingTransformer(currencyIconState = currencyIconState) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) + assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) + assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) + assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) + assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable) + } + + private fun initialState( + actionButtons: ImmutableList = persistentListOf(), + ): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = actionButtons, + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt new file mode 100644 index 0000000000..27a1733679 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt @@ -0,0 +1,481 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +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.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource +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.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 io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class SetBalanceTransformerTest { + + private val onToggleBalanceType: () -> Unit = mockk(relaxed = true) + private val appCurrency: AppCurrency = AppCurrency.Default + + private val network: Network = mockk(relaxed = true) { + every { rawId } returns "ethereum" + } + private val currency: CryptoCurrency = mockk(relaxed = true) { + every { this@mockk.network } returns this@SetBalanceTransformerTest.network + every { symbol } returns "ETH" + } + + @BeforeEach + fun setup() { + mockkStatic(StakingBalance.Data::getTotalWithRewardsStakingBalance) + } + + @AfterEach + fun teardown() { + unmockkStatic(StakingBalance.Data::getTotalWithRewardsStakingBalance) + } + + // region Status type → BalanceBlock type mapping + + @Test + fun `GIVEN Loading status WHEN transform THEN balance block is Loading`() { + // GIVEN + val status = createStatus(CryptoCurrencyStatus.Loading) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Loading::class.java) + } + + @Test + fun `GIVEN Loaded status WHEN transform THEN balance block is Content`() { + // GIVEN + val status = createStatus(loadedValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Content::class.java) + } + + @Test + fun `GIVEN NoQuote status WHEN transform THEN balance block is Content`() { + // GIVEN + val status = createStatus(noQuoteValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Content::class.java) + } + + @Test + fun `GIVEN NoAccount status WHEN transform THEN balance block is Content`() { + // GIVEN + val status = createStatus(noAccountValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Content::class.java) + } + + @Test + fun `GIVEN Custom status WHEN transform THEN balance block is Content`() { + // GIVEN + val status = createStatus(customValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Content::class.java) + } + + @Test + fun `GIVEN Unreachable status WHEN transform THEN balance block is Error`() { + // GIVEN + val status = createStatus( + CryptoCurrencyStatus.Unreachable(priceChange = null, fiatRate = null, networkAddress = null), + ) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Error::class.java) + } + + @Test + fun `GIVEN NoAmount status WHEN transform THEN balance block is Error`() { + // GIVEN + val status = createStatus(CryptoCurrencyStatus.NoAmount(priceChange = null, fiatRate = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Error::class.java) + } + + @Test + fun `GIVEN MissedDerivation status WHEN transform THEN balance block is Error`() { + // GIVEN + val status = createStatus(CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM).isInstanceOf(TokenDetailsBalanceBlockUM.Error::class.java) + } + + // endregion + + // region Action buttons & icon preservation + + @Test + fun `GIVEN any loaded status WHEN transform THEN action buttons are preserved`() { + // GIVEN + val status = createStatus(loadedValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM.actionButtons).isEqualTo(initialState().balanceBlockUM.actionButtons) + } + + @Test + fun `GIVEN any loaded status WHEN transform THEN currency icon state is preserved`() { + // GIVEN + val status = createStatus(loadedValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.balanceBlockUM.currencyIconState) + .isEqualTo(initialState().balanceBlockUM.currencyIconState) + } + + // endregion + + // region Staking / balance type + + @Test + fun `GIVEN loaded status without staking WHEN transform THEN balance type is Single`() { + // GIVEN + val status = createStatus(loadedValue(stakingBalance = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.tokenBalanceTypeUM).isEqualTo(TokenBalanceTypeUM.Single) + } + + @Test + fun `GIVEN loaded status with staking WHEN transform THEN balance type is Multiple`() { + // GIVEN + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.tokenBalanceTypeUM).isInstanceOf(TokenBalanceTypeUM.Multiple::class.java) + } + + @Test + fun `GIVEN loaded status with staking WHEN transform THEN available balance types include ALL and AVAILABLE`() { + // GIVEN + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + assertThat(multiple.availableTypes).containsExactly( + TokenBalanceTypeUM.Type.ALL, + TokenBalanceTypeUM.Type.AVAILABLE, + ) + } + + @Test + fun `GIVEN staking balance WHEN Multiple onSelect invoked THEN onToggleBalanceType is called`() { + // GIVEN + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + multiple.onSelect() + + // THEN + verify(exactly = 1) { onToggleBalanceType.invoke() } + } + + @Test + fun `GIVEN staking and previous Multiple type AVAILABLE WHEN transform THEN selected type is preserved`() { + // GIVEN + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + val prevContent = TokenDetailsBalanceBlockUM.Content( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( + type = TokenBalanceTypeUM.Type.AVAILABLE, + availableTypes = persistentListOf(TokenBalanceTypeUM.Type.ALL, TokenBalanceTypeUM.Type.AVAILABLE), + onSelect = {}, + ), + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference(""), + displayFiatBalanceAll = stringReference(""), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + ) + val state = initialState().copy(balanceBlockUM = prevContent) + + // WHEN + val result = transformer.transform(state) + + // THEN + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + assertThat(multiple.type).isEqualTo(TokenBalanceTypeUM.Type.AVAILABLE) + } + + // endregion + + // region Balance flickering + + @Test + fun `GIVEN CACHE source WHEN transform THEN isBalanceFlickering is true`() { + // GIVEN + val sources = CryptoCurrencyStatus.Sources( + networkSource = StatusSource.CACHE, + quoteSource = StatusSource.CACHE, + stakingBalanceSource = StatusSource.CACHE, + ) + val status = createStatus(loadedValue(sources = sources)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.isBalanceFlickering).isTrue() + } + + @Test + fun `GIVEN ACTUAL source WHEN transform THEN isBalanceFlickering is false`() { + // GIVEN + val sources = CryptoCurrencyStatus.Sources( + networkSource = StatusSource.ACTUAL, + quoteSource = StatusSource.ACTUAL, + stakingBalanceSource = StatusSource.ACTUAL, + ) + val status = createStatus(loadedValue(sources = sources)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.isBalanceFlickering).isFalse() + } + + // endregion + + // region No staking → available balances + + @Test + fun `GIVEN loaded without staking WHEN transform THEN available balances are null`() { + // GIVEN + val status = createStatus(loadedValue(stakingBalance = null)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayCryptoBalanceAvailable).isNull() + assertThat(content.displayFiatBalanceAvailable).isNull() + } + + @Test + fun `GIVEN staking balance WHEN transform THEN available balances are not null`() { + // GIVEN + val stakingBalance: StakingBalance.Data = mockk(relaxed = true) + every { stakingBalance.getTotalWithRewardsStakingBalance(any()) } returns BigDecimal("1.5") + val status = createStatus(loadedValue(stakingBalance = stakingBalance)) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(content.displayCryptoBalanceAvailable).isNotNull() + assertThat(content.displayFiatBalanceAvailable).isNotNull() + } + + // endregion + + // region Unrelated fields preserved + + @Test + fun `GIVEN any status WHEN transform THEN unrelated fields are preserved`() { + // GIVEN + val state = initialState() + val status = createStatus(loadedValue()) + val transformer = createTransformer(status) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) + assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) + assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) + assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) + assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable) + } + + // endregion + + private fun createTransformer(status: CryptoCurrencyStatus) = SetBalanceTransformer( + status = status, + appCurrency = appCurrency, + onToggleBalanceType = onToggleBalanceType, + ) + + private fun createStatus(value: CryptoCurrencyStatus.Value) = CryptoCurrencyStatus( + currency = currency, + value = value, + ) + + private fun loadedValue( + amount: BigDecimal = BigDecimal("10.5"), + fiatAmount: BigDecimal = BigDecimal("21000"), + fiatRate: BigDecimal = BigDecimal("2000"), + stakingBalance: StakingBalance? = null, + sources: CryptoCurrencyStatus.Sources = CryptoCurrencyStatus.Sources(), + ): CryptoCurrencyStatus.Loaded = CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = fiatAmount, + fiatRate = fiatRate, + priceChange = BigDecimal("2.5"), + stakingBalance = stakingBalance, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = mockk(relaxed = true), + sources = sources, + ) + + private fun noQuoteValue(): CryptoCurrencyStatus.NoQuote = CryptoCurrencyStatus.NoQuote( + amount = BigDecimal("5.0"), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = mockk(relaxed = true), + sources = CryptoCurrencyStatus.Sources(), + ) + + private fun noAccountValue(): CryptoCurrencyStatus.NoAccount = CryptoCurrencyStatus.NoAccount( + amountToCreateAccount = BigDecimal("0.01"), + fiatAmount = BigDecimal.ZERO, + priceChange = null, + fiatRate = BigDecimal("2000"), + networkAddress = mockk(relaxed = true), + sources = CryptoCurrencyStatus.Sources(), + ) + + private fun customValue(): CryptoCurrencyStatus.Custom = CryptoCurrencyStatus.Custom( + amount = BigDecimal("100"), + fiatAmount = null, + fiatRate = null, + priceChange = null, + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = mockk(relaxed = true), + sources = CryptoCurrencyStatus.Sources(), + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt new file mode 100644 index 0000000000..2a288fa164 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/ToggleBalanceTypeTransformerTest.kt @@ -0,0 +1,203 @@ +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.extensions.stringReference +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 io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class ToggleBalanceTypeTransformerTest { + + private val transformer = ToggleBalanceTypeTransformer() + + // region Toggle logic + + @Test + fun `GIVEN Multiple with ALL WHEN transform THEN type switches to AVAILABLE`() { + // GIVEN + val state = stateWithContent(type = TokenBalanceTypeUM.Type.ALL) + + // WHEN + val result = transformer.transform(state) + + // THEN + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + assertThat(multiple.type).isEqualTo(TokenBalanceTypeUM.Type.AVAILABLE) + } + + @Test + fun `GIVEN Multiple with AVAILABLE WHEN transform THEN type switches to ALL`() { + // GIVEN + val state = stateWithContent(type = TokenBalanceTypeUM.Type.AVAILABLE) + + // WHEN + val result = transformer.transform(state) + + // THEN + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + assertThat(multiple.type).isEqualTo(TokenBalanceTypeUM.Type.ALL) + } + + @Test + fun `GIVEN Multiple with ALL WHEN transform twice THEN type returns to ALL`() { + // GIVEN + val state = stateWithContent(type = TokenBalanceTypeUM.Type.ALL) + + // WHEN + val result = transformer.transform(transformer.transform(state)) + + // THEN + val multiple = (result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content) + .tokenBalanceTypeUM as TokenBalanceTypeUM.Multiple + assertThat(multiple.type).isEqualTo(TokenBalanceTypeUM.Type.ALL) + } + + // endregion + + // region No-op cases + + @Test + fun `GIVEN Loading balance block WHEN transform THEN state is unchanged`() { + // GIVEN + val state = initialState() + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + } + + @Test + fun `GIVEN Error balance block WHEN transform THEN state is unchanged`() { + // GIVEN + val state = initialState().copy( + balanceBlockUM = TokenDetailsBalanceBlockUM.Error( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + } + + @Test + fun `GIVEN Content with Single balance type WHEN transform THEN state is unchanged`() { + // GIVEN + val state = initialState().copy( + balanceBlockUM = TokenDetailsBalanceBlockUM.Content( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("1.0 ETH"), + displayFiatBalanceAll = stringReference("$2,000"), + displayCryptoBalanceAvailable = null, + displayFiatBalanceAvailable = null, + isBalanceFlickering = false, + ), + ) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result).isSameInstanceAs(state) + } + + // endregion + + // region Unrelated fields preserved + + @Test + fun `GIVEN any togglable state WHEN transform THEN unrelated fields are preserved`() { + // GIVEN + val state = stateWithContent(type = TokenBalanceTypeUM.Type.ALL) + + // WHEN + val result = transformer.transform(state) + + // THEN + assertThat(result.topAppBarUM).isSameInstanceAs(state.topAppBarUM) + assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) + assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) + assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) + assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) + assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable) + } + + @Test + fun `GIVEN togglable state WHEN transform THEN balance content fields besides type are preserved`() { + // GIVEN + val state = stateWithContent(type = TokenBalanceTypeUM.Type.ALL) + val originalContent = state.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + + // WHEN + val result = transformer.transform(state) + + // THEN + val resultContent = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content + assertThat(resultContent.actionButtons).isEqualTo(originalContent.actionButtons) + assertThat(resultContent.currencyIconState).isEqualTo(originalContent.currencyIconState) + assertThat(resultContent.displayCryptoBalanceAll).isEqualTo(originalContent.displayCryptoBalanceAll) + assertThat(resultContent.displayFiatBalanceAll).isEqualTo(originalContent.displayFiatBalanceAll) + assertThat(resultContent.isBalanceFlickering).isEqualTo(originalContent.isBalanceFlickering) + } + + // endregion + + private fun stateWithContent(type: TokenBalanceTypeUM.Type): TokenDetailsUM { + return initialState().copy( + balanceBlockUM = TokenDetailsBalanceBlockUM.Content( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Multiple( + type = type, + availableTypes = persistentListOf( + TokenBalanceTypeUM.Type.ALL, + TokenBalanceTypeUM.Type.AVAILABLE, + ), + onSelect = {}, + ), + currencyIconState = CurrencyIconState.Loading, + displayCryptoBalanceAll = stringReference("10.5 ETH"), + displayFiatBalanceAll = stringReference("$21,000"), + displayCryptoBalanceAvailable = stringReference("9.0 ETH"), + displayFiatBalanceAvailable = stringReference("$18,000"), + isBalanceFlickering = false, + ), + ) + } + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file