Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-24 09:59:56 +02:00
parent 3da4771d66
commit 3baed1e815
57 changed files with 1797 additions and 370 deletions

View file

@ -11,6 +11,7 @@ import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
@ -29,6 +30,7 @@ internal class FeedEntryChildFactory @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
@ -84,6 +86,7 @@ internal class FeedEntryChildFactory @Inject constructor(
portfolioComponentFactory = portfolioComponentFactory,
portfolioBlockComponentFactory = portfolioBlockComponentFactory,
designFeatureToggles = designFeatureToggles,
addToPortfolioComponentFactory = addToPortfolioComponentFactory,
)
}
is Child.TokenList -> {

View file

@ -0,0 +1,7 @@
package com.tangem.features.feed.components.market.details
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal data object AddToPortfolioSlotRoute : Route

View file

@ -16,16 +16,20 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.domain.markets.PreselectedTokenDetailsSection
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
@ -34,10 +38,13 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PreselectedTokenDetailsSection
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockParentClickIntents
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
@ -48,13 +55,15 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@Suppress("LongParameterList")
internal class DefaultMarketsTokenDetailsComponent(
appComponentContext: AppComponentContext,
val params: Params,
analyticsEventHandler: AnalyticsEventHandler,
designFeatureToggles: DesignFeatureToggles,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
val params: Params,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
// applying l2 compatibility
@ -83,12 +92,28 @@ internal class DefaultMarketsTokenDetailsComponent(
if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) {
portfolioBlockComponentFactory.create(
context = child("portfolio_block"),
params = PortfolioBlockComponent.Params(updatedParams.token),
params = PortfolioBlockComponent.Params(token = updatedParams.token),
parentRouter = object : PortfolioBlockParentClickIntents {
override fun openAddToPortfolioDirect() {
model.openAddToPortfolio()
}
override fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
model.openAddToPortfolioViaUserPortfolio(rawCurrencyId)
}
},
)
} else {
null
}
private val addToPortfolioSlot = childSlot(
source = model.addToPortfolioSheetNavigation,
serializer = AddToPortfolioSlotRoute.serializer(),
handleBackButton = false,
childFactory = ::addToPortfolioChild,
)
init {
componentScope.launch(dispatchers.default) {
model.networksState.collectLatest { state ->
@ -120,6 +145,17 @@ internal class DefaultMarketsTokenDetailsComponent(
}
}
private fun addToPortfolioChild(
@Suppress("UNUSED_PARAMETER") config: AddToPortfolioSlotRoute,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val manager = model.addToPortfolioManagerOrNull() ?: return ComposableBottomSheetComponent.EMPTY
return addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(addToPortfolioManager = manager),
)
}
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
@ -198,6 +234,7 @@ internal class DefaultMarketsTokenDetailsComponent(
}
}
val state by model.state.collectAsStateWithLifecycle()
val bottomSheet by addToPortfolioSlot.subscribeAsState()
val bsState by bottomSheetState
LaunchedEffect(bsState) {
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
@ -219,6 +256,7 @@ internal class DefaultMarketsTokenDetailsComponent(
}
},
)
bottomSheet.child?.instance?.BottomSheet()
}
@Serializable

View file

@ -5,24 +5,13 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockRoute
import com.tangem.features.feed.components.market.details.portfolioblock.ui.PortfolioBlock
import com.tangem.features.feed.components.portfolio.PortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -32,26 +21,14 @@ import kotlinx.serialization.Serializable
internal class PortfolioBlockComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: Params,
@Assisted private val parentRouter: PortfolioBlockParentClickIntents?,
) : ComposableContentComponent, AppComponentContext by context {
private val portfolioComponentFactory: PortfolioComponent.Factory = object : PortfolioComponent.Factory {
override fun create(context: AppComponentContext, params: PortfolioComponent.Params): PortfolioComponent {
TODO("STUB. Will be implemented")
}
}
@Serializable
data class Params(
val token: TokenMarketParams,
)
private val model: PortfolioBlockModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = PortfolioBlockRoute.serializer(),
handleBackButton = true,
childFactory = ::createBottomSheetChild,
private val model: PortfolioBlockModel = getOrCreateModel(
PortfolioBlockModelParams(
token = params.token,
parentRouter = parentRouter,
),
)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
@ -65,51 +42,23 @@ internal class PortfolioBlockComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
PortfolioBlock(modifier = modifier, state = state)
bottomSheet.child?.instance?.BottomSheet()
}
@Suppress("UnusedParameter")
private fun createBottomSheetChild(
config: PortfolioBlockRoute,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val currencyId = model.cryptoCurrencyIdState.value ?: return ComposableBottomSheetComponent.EMPTY
val portfolioComponent = portfolioComponentFactory.create(
context = childByContext(componentContext),
params = PortfolioComponent.Params(id = currencyId),
)
return PortfolioBottomSheetWrapper(
portfolioComponent = portfolioComponent,
onDismiss = { model.bottomSheetNavigation.dismiss() },
)
}
private class PortfolioBottomSheetWrapper(
private val portfolioComponent: PortfolioComponent,
private val onDismiss: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismiss()
@Composable
override fun BottomSheet() {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
) {
portfolioComponent.Content(modifier = Modifier)
}
}
}
@Serializable
data class Params(val token: TokenMarketParams)
@AssistedFactory
interface Factory {
fun create(context: AppComponentContext, params: Params): PortfolioBlockComponent
fun create(
context: AppComponentContext,
params: Params,
parentRouter: PortfolioBlockParentClickIntents?,
): PortfolioBlockComponent
}
data class PortfolioBlockModelParams(
val token: TokenMarketParams,
val parentRouter: PortfolioBlockParentClickIntents?,
)
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.feed.components.market.details.portfolioblock
import com.tangem.domain.models.currency.CryptoCurrency
internal interface PortfolioBlockParentClickIntents {
fun openAddToPortfolioDirect()
fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID)
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.feed.components.market.details.portfolioblock.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.SpanStyle
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.getTotalFiatAmount
import com.tangem.core.decompose.di.ModelScoped
@ -26,6 +24,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockParentClickIntents
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
import com.tangem.features.feed.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -50,13 +49,12 @@ internal class PortfolioBlockModel @Inject constructor(
val state: StateFlow<PortfolioBlockUM>
field = MutableStateFlow<PortfolioBlockUM>(PortfolioBlockUM.Loading)
val bottomSheetNavigation: SlotNavigation<PortfolioBlockRoute> = SlotNavigation()
val cryptoCurrencyIdState: StateFlow<CryptoCurrency.ID?>
field = MutableStateFlow(null)
private val params = paramsContainer.require<PortfolioBlockComponent.Params>()
private val params = paramsContainer.require<PortfolioBlockComponent.PortfolioBlockModelParams>()
private val currencyRawId: CryptoCurrency.RawID = params.token.id
private val parentRouter: PortfolioBlockParentClickIntents? = params.parentRouter
private val tokenIcon: CurrencyIconState = CurrencyIconState.CoinIcon(
url = params.token.imageUrl,
@ -114,15 +112,12 @@ internal class PortfolioBlockModel @Inject constructor(
}
}.distinctUntilChanged()
val settingsFlow = combine(
return combine(
portfolioDataFlow,
getSelectedAppCurrencyUseCase.invokeOrDefault(),
getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { appCurrency, isBalanceHidden ->
SettingsBox(appCurrency, isBalanceHidden)
}.distinctUntilChanged()
return combine(portfolioDataFlow, settingsFlow) { portfolios, settings ->
buildState(portfolios, settings)
) { portfolios, appCurrency, isBalanceHidden ->
buildState(portfolios, appCurrency, isBalanceHidden)
}.distinctUntilChanged()
}
@ -135,7 +130,11 @@ internal class PortfolioBlockModel @Inject constructor(
}
}
private fun buildState(portfolios: List<WalletPortfolio>, settings: SettingsBox): PortfolioBlockUM {
private fun buildState(
portfolios: List<WalletPortfolio>,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): PortfolioBlockUM {
val allCurrencies = portfolios.flatMap { it.currencies }
val hasMultiCurrencyWallet = portfolios.any { it.userWallet.isMultiCurrency }
@ -143,36 +142,36 @@ internal class PortfolioBlockModel @Inject constructor(
return if (hasMultiCurrencyWallet) {
PortfolioBlockUM.AddToken(
tokenIcon = tokenIcon,
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
onAddClick = { parentRouter?.openAddToPortfolioDirect() },
)
} else {
PortfolioBlockUM.Hidden
}
}
cryptoCurrencyIdState.update {
it ?: allCurrencies.first().currency.id
}
cryptoCurrencyIdState.update { it ?: allCurrencies.first().currency.id }
val totalFiat = allCurrencies.mapNotNull { it.getTotalFiatAmount() }
.fold(BigDecimal.ZERO, BigDecimal::add)
val formattedBalance = totalFiat.formatStyled {
fiat(
fiatCurrencyCode = settings.appCurrency.code,
fiatCurrencySymbol = settings.appCurrency.symbol,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
}
val firstCurrency = allCurrencies.first().currency
return PortfolioBlockUM.Content(
totalBalance = formattedBalance,
tokensInPortfolioCount = allCurrencies.size,
tokenIcon = tokenIcon,
tokenName = allCurrencies.first().currency.name,
tokenSymbol = allCurrencies.first().currency.symbol,
isBalanceHidden = settings.isBalanceHidden,
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
tokenName = firstCurrency.name,
tokenSymbol = firstCurrency.symbol,
isBalanceHidden = isBalanceHidden,
onRowClick = { parentRouter?.openAddToPortfolioViaUserPortfolio(currencyRawId) },
onAddFundsClick = {},
)
}
}
@ -180,9 +179,4 @@ internal class PortfolioBlockModel @Inject constructor(
private data class WalletPortfolio(
val userWallet: UserWallet,
val currencies: List<CryptoCurrencyStatus>,
)
private data class SettingsBox(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
)

View file

@ -1,7 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolioblock.model
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal data object PortfolioBlockRoute : Route

View file

@ -22,19 +22,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.SpacerW
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.*
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
@ -95,57 +91,53 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi
@Composable
private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) {
FloatingCard(
modifier = modifier,
onClick = state.onClick,
) {
TangemRowContainer(
contentPadding = PaddingValues(0.dp),
FloatingCard(modifier = modifier) { // TODO will be handle in [REDACTED_TASK_KEY]
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
CurrencyIcon(
Row(
modifier = Modifier
.padding(end = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.HEAD),
state = state.tokenIcon,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = state.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodyMedium16,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
text = state.tokenSymbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
.weight(1f)
.clickable(onClick = state.onRowClick),
verticalAlignment = Alignment.CenterVertically,
) {
CurrencyIcon(
modifier = Modifier.padding(end = TangemTheme.dimens2.x3),
state = state.tokenIcon,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodyMedium16,
)
Text(
text = state.tokenSymbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.TAIL),
modifier = Modifier.padding(start = TangemTheme.dimens2.x3),
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
tangemIconUM = TangemIconUM.Icon(
@ -154,7 +146,7 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M
),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onClick,
onClick = state.onAddFundsClick,
),
)
}
@ -163,10 +155,7 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M
@Composable
private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) {
FloatingCard(
modifier = modifier,
onClick = state.onClick,
) {
FloatingCard(modifier = modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
CurrencyIcon(state.tokenIcon)
@ -191,7 +180,7 @@ private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier =
text = resourceReference(R.string.common_add),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onClick,
onClick = state.onAddClick,
),
)
}
@ -200,7 +189,7 @@ private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier =
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FloatingCard(modifier: Modifier = Modifier, onClick: () -> Unit = {}, content: @Composable () -> Unit) {
private fun FloatingCard(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Box(
modifier = modifier
.navigationBarsPadding()
@ -216,7 +205,6 @@ private fun FloatingCard(modifier: Modifier = Modifier, onClick: () -> Unit = {}
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(size = TangemTheme.dimens2.x5),
)
.clickable(onClick = onClick)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
) {
content()
@ -236,7 +224,8 @@ private fun ContentPreview() {
tokenName = "Bitcoin",
tokenSymbol = "BTC",
isBalanceHidden = false,
onClick = {},
onRowClick = {},
onAddFundsClick = {},
),
)
}
@ -250,7 +239,7 @@ private fun AddTokenPreview() {
PortfolioBlock(
state = PortfolioBlockUM.AddToken(
tokenIcon = previewCoinIcon,
onClick = {},
onAddClick = {},
),
)
}

View file

@ -12,7 +12,7 @@ internal sealed class PortfolioBlockUM {
data class AddToken(
val tokenIcon: CurrencyIconState,
val onClick: () -> Unit,
val onAddClick: () -> Unit,
) : PortfolioBlockUM()
data class Content(
@ -22,6 +22,7 @@ internal sealed class PortfolioBlockUM {
val tokenName: String,
val tokenSymbol: String,
val isBalanceHidden: Boolean,
val onClick: () -> Unit,
val onRowClick: () -> Unit,
val onAddFundsClick: () -> Unit,
) : PortfolioBlockUM()
}

View file

@ -1,15 +1,15 @@
package com.tangem.features.feed.components.search
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.models.portfolio.UserAssetEntry
internal sealed interface SearchBottomSheetRoute {
data class TokenSelector(
val entries: List<UserAssetSearchEntry>,
val entries: List<UserAssetEntry>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val onTokenSelected: (UserAssetSearchEntry) -> Unit,
val onTokenSelected: (UserAssetEntry) -> Unit,
val onDismiss: () -> Unit,
) : SearchBottomSheetRoute
}

View file

@ -7,9 +7,9 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.common.ui.markets.tokenselector.TokenSelectorBottomSheet
import com.tangem.features.feed.model.search.SearchTokenSelectorModel
import com.tangem.features.feed.ui.search.components.TokenSelectorBottomSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -37,10 +37,10 @@ internal class SearchTokenSelectorComponent @AssistedInject constructor(
}
data class Params(
val entries: List<UserAssetSearchEntry>,
val entries: List<UserAssetEntry>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val onTokenSelected: (UserAssetSearchEntry) -> Unit,
val onTokenSelected: (UserAssetEntry) -> Unit,
val onDismiss: () -> Unit,
)
}

View file

@ -3,9 +3,13 @@ package com.tangem.features.feed.model.market.details
import androidx.compose.runtime.Stable
import arrow.core.Either
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.TangemSiteShareUrlBuilder
import com.tangem.domain.markets.PreselectedTokenDetailsSection
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.sorted
@ -36,6 +40,7 @@ import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsUseCase
@ -43,6 +48,8 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.feed.components.market.details.AddToPortfolioSlotRoute
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.analytics.MarketTokenAnalyticsEvent
import com.tangem.features.feed.impl.R
@ -65,6 +72,7 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -81,6 +89,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getUserCountryUseCase: GetUserCountryUseCase,
paramsContainer: ParamsContainer,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val designFeatureToggles: DesignFeatureToggles,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
@ -93,6 +102,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private val urlOpener: UrlOpener,
private val getNewsUseCase: GetNewsUseCase,
private val shareManager: ShareManager,
private val appRouter: AppRouter,
) : Model() {
private val quotesJob = JobHolder()
@ -224,6 +234,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
val addToPortfolioSheetNavigation = SlotNavigation<AddToPortfolioSlotRoute>()
private val isAddToPortfolioAvailable: Boolean =
params.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled
private var addToPortfolioManager: AddToPortfolioManager? = null
private var addToPortfolioListenersJob: Job? = null
val state = MutableStateFlow(
MarketsTokenDetailsUM(
tokenName = params.token.name,
@ -309,6 +326,77 @@ internal class MarketsTokenDetailsModel @Inject constructor(
onListedOnClick(exchangesCount)
}
}
modelScope.launch {
networksState.collect { tokenNetworkState ->
val manager = addToPortfolioManager ?: return@collect
when (tokenNetworkState) {
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(tokenNetworkState.networks)
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
else -> Unit
}
}
}
}
fun openAddToPortfolio() {
if (!isAddToPortfolioAvailable) return
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.DirectAdd)
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
}
fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
if (!isAddToPortfolioAvailable) return
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.ViaUserPortfolio(rawCurrencyId))
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
}
private fun openTokenDetails(result: AddToPortfolioManager.Result) {
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = result.wallet.walletId,
currency = result.addedCurrency.currency,
),
)
}
fun addToPortfolioManagerOrNull(): AddToPortfolioManager? = addToPortfolioManager
private fun prepareAddToPortfolioManager(launchMode: AddToPortfolioManager.LaunchMode) {
addToPortfolioListenersJob?.cancel()
val manager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings(
shouldSkipTokenActionsScreen = false,
launchMode = launchMode,
),
analyticsParams = AddToPortfolioManager.AnalyticsParams(params.analyticsParams?.source),
)
manager.setTokenParams(params.token)
when (val network = networksState.value) {
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(network.networks)
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
else -> Unit
}
addToPortfolioManager = manager
addToPortfolioListenersJob = modelScope.launch {
launch {
manager.onDismiss.receiveAsFlow().collect {
addToPortfolioSheetNavigation.dismiss()
}
}
launch {
manager.onSuccessAdded.receiveAsFlow().collect {
addToPortfolioSheetNavigation.dismiss()
}
}
launch {
manager.onAddedTokenClick.receiveAsFlow().collect { result ->
addToPortfolioSheetNavigation.dismiss()
openTokenDetails(result)
}
}
}
}
private fun initialLoad() {

View file

@ -18,7 +18,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.markets.*
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.domain.search.model.UserAssetSearchItem
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
@ -213,7 +213,7 @@ internal class SearchModel @Inject constructor(
stateController.update(UpdateSearchBarQueryTransformer(""))
}
private fun onSingleUserAssetClick(entry: UserAssetSearchEntry) {
private fun onSingleUserAssetClick(entry: UserAssetEntry) {
searchAnalyticsHelper.sendPortfolioItemClicked(entry.currencyStatus.currency.symbol)
appRouter.push(
AppRoute.CurrencyDetails(
@ -236,7 +236,7 @@ internal class SearchModel @Inject constructor(
)
}
private fun onTokenSelectedFromGroup(entry: UserAssetSearchEntry) {
private fun onTokenSelectedFromGroup(entry: UserAssetEntry) {
bottomSheetNavigation.dismiss()
onSingleUserAssetClick(entry)
}

View file

@ -1,13 +1,13 @@
package com.tangem.features.feed.model.search
import androidx.compose.runtime.Stable
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.feed.components.search.SearchTokenSelectorComponent
import com.tangem.features.feed.model.search.state.TokenSelectorStateController
import com.tangem.features.feed.model.search.state.transformers.BuildTokenSelectorSectionsTransformer
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject

View file

@ -1,12 +1,14 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.common.ui.extensions.networkIconResId
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.common.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
@ -14,10 +16,8 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.domain.search.model.UserAssetSearchItem
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
@ -26,7 +26,7 @@ import java.math.BigDecimal
internal class UserAssetSearchItemConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val onSingleClick: (UserAssetSearchEntry) -> Unit,
private val onSingleClick: (UserAssetEntry) -> Unit,
private val onGroupedClick: (UserAssetSearchItem.Grouped) -> Unit,
) : Converter<UserAssetSearchItem, UserAssetItemUM> {
@ -37,7 +37,7 @@ internal class UserAssetSearchItemConverter(
}
}
private fun convertSingle(entry: UserAssetSearchEntry): UserAssetItemUM.Single {
private fun convertSingle(entry: UserAssetEntry): UserAssetItemUM.Single {
val currency = entry.currencyStatus.currency
val value = entry.currencyStatus.value
@ -124,7 +124,7 @@ internal class UserAssetSearchItemConverter(
}
private fun convertGroupedBalanceState(
entries: List<UserAssetSearchEntry>,
entries: List<UserAssetEntry>,
symbol: String,
decimals: Int,
): BalanceDisplayState {
@ -145,7 +145,7 @@ internal class UserAssetSearchItemConverter(
}
private fun computeGroupBalance(
entries: List<UserAssetSearchEntry>,
entries: List<UserAssetEntry>,
symbol: String,
decimals: Int,
): BalanceDisplayState.Loaded {
@ -158,7 +158,7 @@ internal class UserAssetSearchItemConverter(
}
private fun computeGroupBalanceFlickering(
entries: List<UserAssetSearchEntry>,
entries: List<UserAssetEntry>,
symbol: String,
decimals: Int,
): BalanceDisplayState.Flickering {

View file

@ -1,8 +1,8 @@
package com.tangem.features.feed.model.search.state
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.features.feed.model.search.state.transformers.TokenSelectorUMTransformer
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View file

@ -1,18 +1,18 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.markets.tokenselector.AccountHeaderData
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.features.feed.ui.search.state.AccountHeaderData
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
import com.tangem.domain.models.portfolio.UserAssetEntry
import kotlinx.collections.immutable.toImmutableList
internal class BuildTokenSelectorSectionsTransformer(
private val entries: List<UserAssetSearchEntry>,
private val entries: List<UserAssetEntry>,
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val onTokenSelected: (UserAssetSearchEntry) -> Unit,
private val onTokenSelected: (UserAssetEntry) -> Unit,
) : TokenSelectorUMTransformer {
private val entryConverter = TokenSelectorEntryConverter(

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
@ -11,9 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
@ -22,12 +22,12 @@ import java.math.BigDecimal
internal class TokenSelectorEntryConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val onTokenSelected: (UserAssetSearchEntry) -> Unit,
) : Converter<UserAssetSearchEntry, UserAssetItemUM.Single> {
private val onTokenSelected: (UserAssetEntry) -> Unit,
) : Converter<UserAssetEntry, UserAssetItemUM.Single> {
private val iconConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: UserAssetSearchEntry): UserAssetItemUM.Single {
override fun convert(value: UserAssetEntry): UserAssetItemUM.Single {
val currency = value.currencyStatus.currency
val currencyValue = value.currencyStatus.value

View file

@ -1,6 +1,6 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
internal interface TokenSelectorUMTransformer {
fun transform(prevState: TokenSelectorContentUM): TokenSelectorContentUM

View file

@ -1,9 +1,9 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.features.feed.ui.search.state.MarketSearchResultUM
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import kotlinx.collections.immutable.ImmutableList
internal class UpdateUserAssetsTransformer(

View file

@ -1,99 +0,0 @@
package com.tangem.features.feed.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private const val MIN_STACKED_ICON_COUNT = 1
private const val MAX_STACKED_ICON_COUNT = 3
private const val FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 1
private const val SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 2
private const val MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER = 2
private const val FIRST_BACK_LAYER_ICON_ALPHA = 0.4f
private const val SECOND_BACK_LAYER_ICON_ALPHA = 0.2f
@Composable
fun LayeringIcons(
tangemIconUM: TangemIconUM,
modifier: Modifier = Modifier,
count: Int = MIN_STACKED_ICON_COUNT,
layerHorizontalShift: Dp = TangemTheme.dimens2.x1,
iconSize: Dp = TangemTheme.dimens2.x10,
) {
require(count > 0)
val stackTrailingWidth = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER
Box(
modifier = modifier.size(
width = iconSize + stackTrailingWidth,
height = iconSize,
),
) {
val baseIconModifier = Modifier
.align(Alignment.TopStart)
.size(iconSize)
if (count >= MAX_STACKED_ICON_COUNT) {
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier
.offset(x = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
.alpha(SECOND_BACK_LAYER_ICON_ALPHA),
)
}
if (count >= MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER) {
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier
.offset(x = layerHorizontalShift * FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
.alpha(FIRST_BACK_LAYER_ICON_ALPHA),
)
}
TangemIcon(
tangemIconUM = tangemIconUM,
modifier = baseIconModifier,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun LayeringIconsPreview() {
val previewCurrencyIcon = TangemIconUM.Currency(
currencyIconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = null,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
)
TangemThemePreview {
Column(horizontalAlignment = Alignment.End) {
LayeringIcons(count = MIN_STACKED_ICON_COUNT, tangemIconUM = previewCurrencyIcon)
LayeringIcons(
count = MAX_STACKED_ICON_COUNT,
tangemIconUM = previewCurrencyIcon,
)
LayeringIcons(
count = MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER,
tangemIconUM = previewCurrencyIcon,
)
}
}
}

View file

@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.MarketsListItem
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.markets.tokenselector.GroupedUserAssetItem
import com.tangem.common.ui.markets.tokenselector.SingleUserAssetItem
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
@ -33,8 +36,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
import com.tangem.features.feed.ui.search.state.*
import kotlinx.collections.immutable.ImmutableList

View file

@ -1,175 +0,0 @@
package com.tangem.features.feed.ui.search.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.utils.StringsSigns
@Composable
internal fun BalanceColumn(
balanceState: BalanceDisplayState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
if (isBalanceHidden) {
HiddenBalance(modifier)
return
}
when (balanceState) {
is BalanceDisplayState.Loading -> LoadingBalanceColumn(modifier)
is BalanceDisplayState.Flickering -> FlickeringBalanceColumn(balanceState, modifier)
is BalanceDisplayState.Stale -> StaleBalanceColumn(balanceState, modifier)
is BalanceDisplayState.Unreachable -> UnreachableBalanceColumn(modifier)
is BalanceDisplayState.Loaded -> LoadedBalanceColumn(balanceState, modifier)
}
}
@Composable
private fun BalanceColumnLayout(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp),
content = content,
)
}
@Composable
private fun LoadingBalanceColumn(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
RectangleShimmer(modifier = Modifier.size(width = 108.dp, height = 20.dp), radius = 20.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 16.dp), radius = 20.dp)
}
}
@Composable
private fun FlickeringBalanceColumn(state: BalanceDisplayState.Flickering, modifier: Modifier = Modifier) {
val flickerModifier = Modifier.flicker(isFlickering = true)
BalanceColumnLayout(modifier) {
Text(
modifier = flickerModifier,
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = flickerModifier,
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun StaleBalanceColumn(state: BalanceDisplayState.Stale, modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.ic_error_sync_24),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconGray,
)
SpacerW(TangemTheme.dimens2.x0_5)
Text(
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Text(
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun UnreachableBalanceColumn(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = StringsSigns.DASH_SIGN,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.common_unreachable),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.status.attention,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerW(TangemTheme.dimens2.x0_5)
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.ic_alert_triange_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.status.attention,
)
}
}
}
@Composable
private fun LoadedBalanceColumn(state: BalanceDisplayState.Loaded, modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = state.fiatBalance.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = state.cryptoBalance.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}
@Composable
private fun HiddenBalance(modifier: Modifier = Modifier) {
BalanceColumnLayout(modifier) {
Text(
text = StringsSigns.THREE_STARS,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = StringsSigns.THREE_STARS,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}

View file

@ -1,84 +0,0 @@
package com.tangem.features.feed.ui.search.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.components.LayeringIcons
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
@Composable
internal fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
) {
TangemRowContainer(
modifier = Modifier.clickable(onClick = item.onClick),
content = {
LayeringIcons(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x1),
tangemIconUM = item.icon,
count = item.tokensCount,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = item.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = pluralStringResourceSafe(R.plurals.common_tokens_count, item.tokensCount, item.tokensCount),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
BalanceColumn(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
balanceState = item.balanceState,
isBalanceHidden = item.isBalanceHidden,
)
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x2)
.layoutId(TangemRowLayoutId.TAIL),
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
tangemIconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_chevron_24,
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X10,
onClick = item.onClick,
),
)
},
)
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.features.feed.ui.search.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
@Composable
fun SingleUserAssetItem(shouldUsePriceBlock: Boolean, item: UserAssetItemUM.Single, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier.clickable(onClick = item.onClick),
content = {
TangemIcon(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.size(40.dp)
.padding(end = TangemTheme.dimens2.x1),
tangemIconUM = item.icon,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = item.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
if (shouldUsePriceBlock) {
PriceBlock(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
priceChangeState = item.priceChangeState,
fiatRate = item.fiatRate,
balanceState = item.balanceState,
)
} else {
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = item.networkName,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
}
BalanceColumn(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
balanceState = item.balanceState,
isBalanceHidden = item.isBalanceHidden,
)
},
)
}
@Composable
private fun PriceBlock(
priceChangeState: PriceChangeState,
fiatRate: String?,
balanceState: BalanceDisplayState,
modifier: Modifier = Modifier,
) {
val isDisabled = remember(balanceState) {
balanceState is BalanceDisplayState.Unreachable
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
if (fiatRate != null) {
Text(
text = fiatRate,
style = TangemTheme.typography2.captionMedium12,
color = if (isDisabled) {
TangemTheme.colors2.text.status.disabled
} else {
TangemTheme.colors2.text.neutral.secondary
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
AnimatedContent(
targetState = priceChangeState,
contentKey = { it::class },
) { animatedState ->
when (animatedState) {
is PriceChangeState.Content -> {
PriceChangeInPercent(
valueInPercent = animatedState.valueInPercent,
type = animatedState.type,
textStyle = TangemTheme.typography2.captionMedium12,
isDisabled = isDisabled,
)
}
PriceChangeState.Unknown -> Unit
}
}
}
}

View file

@ -1,146 +0,0 @@
package com.tangem.features.feed.ui.search.components
import android.content.res.Configuration
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Fade
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.ui.search.preview.TokenSelectorContentPreviewProvider
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.rememberHazeState
@Composable
internal fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<TokenSelectorContentUM>(
config = config,
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors2.surface.level2,
content = { content ->
TokenSelectorContent(content, config.onDismissRequest)
},
)
}
@Composable
private fun TokenSelectorContent(content: TokenSelectorContentUM, onDismiss: () -> Unit) {
val hazeState = rememberHazeState()
var topBarHeight by remember { mutableStateOf(0.dp) }
Box(modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.hazeSourceTangem(state = hazeState, 1f),
contentPadding = PaddingValues(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
top = topBarHeight,
bottom = TangemTheme.dimens2.x10,
),
) {
tokenSelectorSectionItems(content.sections)
}
TokenSelectorSheetTopBar(
modifier = Modifier.align(Alignment.TopEnd),
onDismiss = onDismiss,
hazeState = hazeState,
onChangeHeight = { topBarHeight = it },
)
Fade(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
height = TangemTheme.dimens2.x10,
)
}
}
@Composable
private fun TokenSelectorSheetTopBar(
hazeState: HazeState,
onDismiss: () -> Unit,
onChangeHeight: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
val bgColor = TangemTheme.colors2.surface.level2
val density = LocalDensity.current
TangemTopBar(
modifier = modifier
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
onChangeHeight(coordinates.size.height.toDp())
}
}
}
.hazeEffectTangem(state = hazeState) {
backgroundColor = bgColor
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
title = resourceReference(R.string.markets_search_portfolio_header),
endContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = onDismiss)
.padding(TangemTheme.dimens2.x2_5),
)
},
)
}
@Preview
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TokenSelectorBottomSheetPreview(
@PreviewParameter(TokenSelectorContentPreviewProvider::class) content: TokenSelectorContentUM,
) {
TangemThemePreviewRedesign {
TokenSelectorBottomSheet(
config = TangemBottomSheetConfig(
onDismissRequest = {},
content = content,
isShown = true,
),
)
}
}

View file

@ -1,140 +0,0 @@
package com.tangem.features.feed.ui.search.components
import androidx.compose.foundation.layout.Arrangement
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.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.common.ui.account.getResId
import com.tangem.common.ui.account.getUiColor
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.state.AccountHeaderData
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
import kotlinx.collections.immutable.ImmutableList
internal fun LazyListScope.tokenSelectorSectionItems(sections: ImmutableList<TokenSelectorSectionUM>) {
sections.forEachIndexed { index, section ->
when (section) {
is TokenSelectorSectionUM.WalletHeader -> {
item(key = "wallet_${section.walletName}_$index") {
WalletHeaderSection(section)
}
}
is TokenSelectorSectionUM.TokenGroup -> {
if (section.items.isEmpty()) return@forEachIndexed
if (index > 0 && sections[index - 1] is TokenSelectorSectionUM.TokenGroup) {
item(key = "spacer_before_group_$index") {
SpacerH(TangemTheme.dimens2.x2)
}
}
val lastIndex = if (section.accountHeader != null) {
section.items.size
} else {
section.items.lastIndex.coerceAtLeast(0)
}
section.accountHeader?.let { header ->
item(key = "account_header_$index") {
TokenGroupAccountHeaderRow(
data = header,
modifier = Modifier.tokenGroupRowDecoration(
currentIndex = 0,
lastIndex = lastIndex,
),
)
}
}
val indexOffset = if (section.accountHeader != null) 1 else 0
section.items.forEachIndexed { itemIndex, single ->
item(key = "token_${single.id}_$index") {
SingleUserAssetItem(
item = single,
modifier = Modifier.tokenGroupRowDecoration(
currentIndex = indexOffset + itemIndex,
lastIndex = lastIndex,
),
shouldUsePriceBlock = false,
)
}
}
}
}
}
}
@Composable
private fun Modifier.tokenGroupRowDecoration(currentIndex: Int, lastIndex: Int): Modifier =
this.roundedShapeItemDecoration(
currentIndex = currentIndex,
lastIndex = lastIndex,
addDefaultPadding = false,
radius = TangemTheme.dimens2.x6,
backgroundColor = TangemTheme.colors2.surface.level3,
)
@Composable
private fun WalletHeaderSection(section: TokenSelectorSectionUM.WalletHeader) {
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
.padding(horizontal = TangemTheme.dimens2.x3),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Text(
text = section.walletName,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_key_card_20),
modifier = Modifier.size(TangemTheme.dimens2.x5),
tint = TangemTheme.colors2.graphic.neutral.tertiary,
contentDescription = null,
)
}
}
@Composable
private fun TokenGroupAccountHeaderRow(data: AccountHeaderData, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
.padding(horizontal = TangemTheme.dimens2.x4),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
imageVector = ImageVector.vectorResource(data.cryptoPortfolioIcon.value.getResId()),
tint = data.cryptoPortfolioIcon.color.getUiColor(),
contentDescription = null,
)
Text(
text = data.accountName.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType

View file

@ -1,122 +0,0 @@
package com.tangem.features.feed.ui.search.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.feed.ui.search.state.AccountHeaderData
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import kotlinx.collections.immutable.persistentListOf
@Suppress("StringLiteralDuplication")
internal class TokenSelectorContentPreviewProvider :
CollectionPreviewParameterProvider<TokenSelectorContentUM>(
listOf(
tokenSelectorPreviewSimple(),
tokenSelectorPreviewWithAccountHeaders(),
tokenSelectorPreviewMultiWallet(),
),
)
private fun tokenSelectorPreviewSimple(): TokenSelectorContentUM {
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "eth", name = "Ethereum", symbol = "ETH"),
previewTokenItem(id = "btc", name = "Bitcoin", symbol = "BTC"),
),
),
),
)
}
private fun tokenSelectorPreviewWithAccountHeaders(): TokenSelectorContentUM {
val accountIcon = CryptoPortfolioIcon.ofCustomAccount(
value = CryptoPortfolioIcon.Icon.Wallet,
color = CryptoPortfolioIcon.Color.CaribbeanBlue,
)
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.TokenGroup(
accountHeader = AccountHeaderData(
accountName = stringReference(value = "Main account"),
cryptoPortfolioIcon = accountIcon,
),
items = persistentListOf(
previewTokenItem(id = "eth_main", name = "Ethereum", symbol = "ETH"),
),
),
TokenSelectorSectionUM.TokenGroup(
accountHeader = AccountHeaderData(
accountName = stringReference(value = "Trading"),
cryptoPortfolioIcon = accountIcon,
),
items = persistentListOf(
previewTokenItem(id = "sol_trade", name = "Solana", symbol = "SOL"),
previewTokenItem(id = "avax_trade", name = "Avalanche", symbol = "AVAX"),
),
),
),
)
}
private fun tokenSelectorPreviewMultiWallet(): TokenSelectorContentUM {
return TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.WalletHeader(walletName = "Cold wallet"),
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "btc_cold", name = "Bitcoin", symbol = "BTC"),
),
),
TokenSelectorSectionUM.WalletHeader(walletName = "Hot wallet"),
TokenSelectorSectionUM.TokenGroup(
accountHeader = null,
items = persistentListOf(
previewTokenItem(id = "eth_hot", name = "Ethereum", symbol = "ETH"),
previewTokenItem(id = "usdt_hot", name = "Tether", symbol = "USDT"),
),
),
),
)
}
private fun previewTokenItem(id: String, name: String, symbol: String): UserAssetItemUM.Single {
val cryptoRef = stringReference(value = "1.234 $symbol")
val fiatRef = stringReference(value = "$1,234.56")
return UserAssetItemUM.Single(
id = id,
icon = TangemIconUM.Currency(
CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.ic_ethereumpow_22,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
tokenName = name,
tokenSymbol = symbol,
fiatRate = "$98,765.43",
priceChangeState = PriceChangeState.Content(
valueInPercent = "+2.34%",
type = PriceChangeType.UP,
),
balanceState = BalanceDisplayState.Loaded(
cryptoBalance = cryptoRef,
fiatBalance = fiatRef,
),
isBalanceHidden = false,
onClick = {},
networkName = "Ethereum",
)
}

View file

@ -19,10 +19,10 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
import com.tangem.common.ui.markets.tokenselector.GroupedUserAssetItem
import com.tangem.common.ui.markets.tokenselector.SingleUserAssetItem
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
/** Labeled UI state for [SingleUserAssetItem] previews (dropdown label in Studio). */
internal data class SingleUserAssetItemPreviewScenario(

View file

@ -2,10 +2,8 @@ package com.tangem.features.feed.ui.search.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class SearchUM(
@ -43,59 +41,4 @@ sealed interface MarketSearchResultUM {
data object Empty : MarketSearchResultUM
}
data class TextHintItemUM(val text: String)
@Immutable
sealed interface BalanceDisplayState {
data class Loaded(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data class Flickering(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data class Stale(
val cryptoBalance: TextReference,
val fiatBalance: TextReference,
) : BalanceDisplayState
data object Loading : BalanceDisplayState
data object Unreachable : BalanceDisplayState
}
@Immutable
sealed interface UserAssetItemUM {
val id: String
val icon: TangemIconUM
val tokenName: String
val tokenSymbol: String
val onClick: () -> Unit
data class Single(
override val id: String,
override val icon: TangemIconUM,
override val tokenName: String,
override val tokenSymbol: String,
val fiatRate: String?,
val priceChangeState: PriceChangeState,
val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
val networkName: String,
override val onClick: () -> Unit,
) : UserAssetItemUM
data class Grouped(
override val id: String,
override val icon: TangemIconUM,
override val tokenName: String,
override val tokenSymbol: String,
val tokensCount: Int,
val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
override val onClick: () -> Unit,
) : UserAssetItemUM
}
data class TextHintItemUM(val text: String)

View file

@ -1,29 +0,0 @@
package com.tangem.features.feed.ui.search.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class TokenSelectorContentUM(
val sections: ImmutableList<TokenSelectorSectionUM>,
) : TangemBottomSheetConfigContent
@Immutable
internal data class AccountHeaderData(
val accountName: TextReference,
val cryptoPortfolioIcon: CryptoPortfolioIcon,
)
@Immutable
internal sealed interface TokenSelectorSectionUM {
data class WalletHeader(val walletName: String) : TokenSelectorSectionUM
data class TokenGroup(
val accountHeader: AccountHeaderData?,
val items: ImmutableList<UserAssetItemUM.Single>,
) : TokenSelectorSectionUM
}

View file

@ -8,9 +8,9 @@ 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.wallet.UserWalletId
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import io.mockk.*
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.BeforeEach
@ -22,7 +22,7 @@ import java.math.BigDecimal
class BuildTokenSelectorSectionsTransformerTest {
private val appCurrency: AppCurrency = AppCurrency.Default
private val onTokenSelected: (UserAssetSearchEntry) -> Unit = mockk(relaxed = true)
private val onTokenSelected: (UserAssetEntry) -> Unit = mockk(relaxed = true)
private val prevState = TokenSelectorContentUM(sections = persistentListOf())
@BeforeEach
@ -231,7 +231,7 @@ class BuildTokenSelectorSectionsTransformerTest {
// region Helpers
private fun createTransformer(entries: List<UserAssetSearchEntry>): BuildTokenSelectorSectionsTransformer {
private fun createTransformer(entries: List<UserAssetEntry>): BuildTokenSelectorSectionsTransformer {
return BuildTokenSelectorSectionsTransformer(
entries = entries,
appCurrency = appCurrency,
@ -259,7 +259,7 @@ class BuildTokenSelectorSectionsTransformerTest {
currencyId: String = "btc",
currencyName: String = "Bitcoin",
currencySymbol: String = "BTC",
): UserAssetSearchEntry {
): UserAssetEntry {
val network = mockk<Network>(relaxed = true) {
every { name } returns "Network"
}
@ -290,7 +290,7 @@ class BuildTokenSelectorSectionsTransformerTest {
sources = CryptoCurrencyStatus.Sources(),
)
}
return mockk<UserAssetSearchEntry> {
return mockk<UserAssetEntry> {
every { userWalletId } returns walletId
every { userWalletName } returns walletName
every { this@mockk.accountId } returns accountId

View file

@ -8,8 +8,8 @@ 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.wallet.UserWalletId
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
@ -23,7 +23,7 @@ import java.math.BigDecimal
class TokenSelectorEntryConverterTest {
private val appCurrency: AppCurrency = AppCurrency.Default
private val onTokenSelected: (UserAssetSearchEntry) -> Unit = mockk(relaxed = true)
private val onTokenSelected: (UserAssetEntry) -> Unit = mockk(relaxed = true)
private lateinit var converter: TokenSelectorEntryConverter
@ -276,7 +276,7 @@ class TokenSelectorEntryConverterTest {
fiatRate = BigDecimal("30000.0"),
priceChange = BigDecimal("1.0"),
),
): UserAssetSearchEntry {
): UserAssetEntry {
val userWalletId = mockk<UserWalletId> {
every { stringValue } returns walletId
}
@ -302,7 +302,7 @@ class TokenSelectorEntryConverterTest {
every { this@mockk.currency } returns currency
every { this@mockk.value } returns value
}
return mockk<UserAssetSearchEntry> {
return mockk<UserAssetEntry> {
every { this@mockk.userWalletId } returns userWalletId
every { this@mockk.userWalletName } returns "Wallet"
every { this@mockk.accountId } returns accountIdMock