Updated on 2026-08-14
This commit is contained in:
parent
236794fa00
commit
ce5f535ac5
15 changed files with 1385 additions and 34 deletions
|
|
@ -1707,6 +1707,8 @@
|
|||
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not supported by current providers, but we are working to add more options.</string>
|
||||
<string name="token_button_unavailability_reason_staking_unavailable">Staking %s is not supported by current providers, but we are working to add more options.</string>
|
||||
<string name="token_button_unavailability_reason_yield_supply_approval">Approval has been revoked. Your funds remain in Yield mode. To perform actions, please go to Yield mode and grant permission again.</string>
|
||||
<string name="token_details_balance_available">Available balance</string>
|
||||
<string name="token_details_balance_total">Total balance</string>
|
||||
<string name="token_details_generate_xpub">Generate XPUB</string>
|
||||
<string name="token_details_hide_alert_hide">Hide</string>
|
||||
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<AppCurrency> = 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(
|
||||
|
|
|
|||
|
|
@ -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<TangemButtonUM>,
|
||||
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<TangemButtonUM>,
|
||||
|
|
@ -34,11 +51,11 @@ internal sealed class TokenDetailsBalanceBlockUM {
|
|||
override val currencyIconState: CurrencyIconState,
|
||||
) : TokenDetailsBalanceBlockUM()
|
||||
|
||||
fun copyActionButtons(buttons: ImmutableList<TangemButtonUM>): 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<Type>,
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
|
||||
val prevBalance = prevState.balanceBlockUM
|
||||
return prevState.copy(
|
||||
balanceBlockUM = TokenDetailsBalanceBlockUM.Loading(
|
||||
actionButtons = prevBalance.actionButtons,
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = currencyIconState,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenDetailsUM> {
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
),
|
||||
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,
|
||||
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
|
||||
|
|
@ -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<TokenDetailsBalanceBlockUM>(relaxed = true),
|
||||
balanceBlockUM = TokenDetailsBalanceBlockUM.Loading(
|
||||
actionButtons = persistentListOf(),
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = mockk(relaxed = true),
|
||||
),
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
|
|
|
|||
|
|
@ -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<TangemButtonUM> = 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<MarketPriceBlockState>(relaxed = true),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MarketPriceBlockState>(relaxed = true),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MarketPriceBlockState>(relaxed = true),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue