From 3baed1e81567d0603381e113163dd8a24d99e477 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Apr 2026 09:59:56 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../markets/tokenselector}/BalanceColumn.kt | 9 +- .../tokenselector}/GroupedUserAssetItem.kt | 8 +- .../markets/tokenselector}/LayeringIcons.kt | 2 +- .../tokenselector}/SingleUserAssetItem.kt | 4 +- .../TokenSelectorBottomSheet.kt | 87 ++- .../TokenSelectorContentPreviewProvider.kt | 9 +- .../tokenselector/TokenSelectorContentUM.kt | 8 +- .../tokenselector}/TokenSelectorList.kt | 6 +- .../markets/tokenselector/UserAssetItemUM.kt | 61 ++ .../models/portfolio/UserAssetEntry.kt} | 4 +- .../search/model/UserAssetSearchItem.kt | 6 +- .../search/usecase/GetSearchResultsUseCase.kt | 8 +- .../addtoportfolio/AddToPortfolioComponent.kt | 4 +- .../addtoportfolio/AddToPortfolioManager.kt | 30 +- .../common-features/impl/build.gradle.kts | 12 + .../AddToPortfolioBottomSheet.kt | 7 +- .../DefaultAddToPortfolioComponent.kt | 20 +- ...tAddToPortfolioPreselectedDataComponent.kt | 1 + .../di/AddToPortfolioComponentModule.kt | 7 + .../di/AddToPortfolioModelModule.kt | 6 + .../AddToPortfolioInitialSelectionResolver.kt | 114 ++++ .../model/AddToPortfolioModel.kt | 226 ++++++- .../model/AddToPortfolioRoutes.kt | 3 + .../ui/DefaultAddToPortfolioManager.kt | 5 + .../DefaultUserPortfolioComponent.kt | 47 ++ .../userportfolio/UserPortfolioComponent.kt | 20 + .../userportfolio/model/UserPortfolioModel.kt | 20 + .../userportfolio/model/UserPortfolioUM.kt | 10 + .../state/UserPortfolioStateController.kt | 57 ++ .../UserPortfolioSectionsTransformer.kt | 187 ++++++ ...ToPortfolioInitialSelectionResolverTest.kt | 591 ++++++++++++++++++ .../feed/components/FeedEntryChildFactory.kt | 3 + .../market/details/AddToPortfolioSlotRoute.kt | 7 + .../DefaultMarketsTokenDetailsComponent.kt | 44 +- .../portfolioblock/PortfolioBlockComponent.kt | 87 +-- .../PortfolioBlockParentClickIntents.kt | 8 + .../model/PortfolioBlockModel.kt | 50 +- .../model/PortfolioBlockRoute.kt | 7 - .../portfolioblock/ui/PortfolioBlock.kt | 113 ++-- .../ui/state/PortfolioBlockUM.kt | 5 +- .../search/SearchBottomSheetRoute.kt | 6 +- .../search/SearchTokenSelectorComponent.kt | 8 +- .../details/MarketsTokenDetailsModel.kt | 90 ++- .../features/feed/model/search/SearchModel.kt | 6 +- .../model/search/SearchTokenSelectorModel.kt | 2 +- .../converter/UserAssetSearchItemConverter.kt | 22 +- .../state/TokenSelectorStateController.kt | 2 +- .../BuildTokenSelectorSectionsTransformer.kt | 12 +- .../TokenSelectorEntryConverter.kt | 12 +- .../TokenSelectorUMTransformer.kt | 2 +- .../UpdateUserAssetsTransformer.kt | 2 +- .../features/feed/ui/search/SearchContent.kt | 5 +- .../ui/search/preview/SearchContentPreview.kt | 2 + .../ui/search/preview/UserAssetItemPreview.kt | 8 +- .../features/feed/ui/search/state/SearchUM.kt | 61 +- ...ildTokenSelectorSectionsTransformerTest.kt | 14 +- .../TokenSelectorEntryConverterTest.kt | 10 +- 57 files changed, 1797 insertions(+), 370 deletions(-) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/BalanceColumn.kt (96%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/GroupedUserAssetItem.kt (91%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/LayeringIcons.kt (98%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/SingleUserAssetItem.kt (95%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/TokenSelectorBottomSheet.kt (63%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/TokenSelectorContentPreviewProvider.kt (90%) rename features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/TokenSelectorUM.kt => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentUM.kt (80%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector}/TokenSelectorList.kt (94%) create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/UserAssetItemUM.kt rename domain/{search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt => models/src/main/kotlin/com/tangem/domain/models/portfolio/UserAssetEntry.kt} (87%) create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/UserPortfolioComponent.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioModel.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioUM.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/state/UserPortfolioStateController.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/transformer/UserPortfolioSectionsTransformer.kt create mode 100644 features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/AddToPortfolioSlotRoute.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockParentClickIntents.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockRoute.kt diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/BalanceColumn.kt similarity index 96% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/BalanceColumn.kt index 552f07fcb3..36625d5b13 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/BalanceColumn.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.components +package com.tangem.common.ui.markets.tokenselector import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -17,15 +17,10 @@ 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, -) { +fun BalanceColumn(balanceState: BalanceDisplayState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { if (isBalanceHidden) { HiddenBalance(modifier) return diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/GroupedUserAssetItem.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/GroupedUserAssetItem.kt index 11504f43d5..26115886e6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/GroupedUserAssetItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.components +package com.tangem.common.ui.markets.tokenselector import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -10,18 +10,16 @@ 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.R 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) { +fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) { Box( modifier = modifier.background( color = TangemTheme.colors2.surface.level3, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/LayeringIcons.kt similarity index 98% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/LayeringIcons.kt index 1236344817..2ca0775133 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/LayeringIcons.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.components +package com.tangem.common.ui.markets.tokenselector import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/SingleUserAssetItem.kt similarity index 95% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/SingleUserAssetItem.kt index dd388ce1c5..62d8e486f5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/SingleUserAssetItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.components +package com.tangem.common.ui.markets.tokenselector import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.clickable @@ -17,8 +17,6 @@ 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) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorBottomSheet.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt similarity index 63% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorBottomSheet.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt index 36196001f9..b9d7920386 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorBottomSheet.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.components +package com.tangem.common.ui.markets.tokenselector import android.content.res.Configuration import androidx.compose.animation.core.EaseOut @@ -25,59 +25,114 @@ 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.button.* import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.extensions.TextReference 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) { +fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig, stickyFooter: StickyFooter? = null) { TangemBottomSheet( config = config, type = TangemBottomSheetType.Modal, containerColor = TangemTheme.colors2.surface.level2, content = { content -> - TokenSelectorContent(content, config.onDismissRequest) + TokenSelectorContent( + content = content, + onDismiss = config.onDismissRequest, + stickyFooter = stickyFooter, + embedded = false, + ) }, ) } +data class StickyFooter(val buttonText: TextReference, val isEnabled: Boolean = true, val onClick: () -> Unit) + @Composable -private fun TokenSelectorContent(content: TokenSelectorContentUM, onDismiss: () -> Unit) { +fun TokenSelectorEmbeddedContent( + content: TokenSelectorContentUM, + stickyFooter: StickyFooter?, + modifier: Modifier = Modifier, +) { + TokenSelectorContent( + content = content, + onDismiss = {}, + stickyFooter = stickyFooter, + embedded = true, + modifier = modifier, + ) +} + +@Composable +private fun TokenSelectorContent( + content: TokenSelectorContentUM, + onDismiss: () -> Unit, + stickyFooter: StickyFooter?, + embedded: Boolean, + modifier: Modifier = Modifier, +) { val hazeState = rememberHazeState() var topBarHeight by remember { mutableStateOf(0.dp) } + val topContentPadding = if (embedded) { + TangemTheme.dimens2.x4 + } else { + topBarHeight + } + val footerHeight = TangemTheme.dimens2.x14 + TangemTheme.dimens2.x4 + val listBottomPadding = TangemTheme.dimens2.x10 + if (stickyFooter != null) footerHeight else 0.dp - Box(modifier = Modifier.fillMaxWidth()) { + 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, + top = topContentPadding, + bottom = listBottomPadding, ), ) { tokenSelectorSectionItems(content.sections) } - TokenSelectorSheetTopBar( - modifier = Modifier.align(Alignment.TopEnd), - onDismiss = onDismiss, - hazeState = hazeState, - onChangeHeight = { topBarHeight = it }, - ) + if (!embedded) { + TokenSelectorSheetTopBar( + modifier = Modifier.align(Alignment.TopEnd), + onDismiss = onDismiss, + hazeState = hazeState, + onChangeHeight = { topBarHeight = it }, + ) + } Fade( modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter), + .align(Alignment.BottomCenter) + .padding(bottom = if (stickyFooter != null) footerHeight else 0.dp), height = TangemTheme.dimens2.x10, ) + if (stickyFooter != null) { + TangemButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2) + .fillMaxWidth(), + buttonUM = TangemButtonUM( + type = TangemButtonType.Primary, + text = stickyFooter.buttonText, + shape = TangemButtonShape.Rounded, + size = TangemButtonSize.X15, + isEnabled = stickyFooter.isEnabled, + onClick = stickyFooter.onClick, + ), + ) + } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/TokenSelectorContentPreviewProvider.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentPreviewProvider.kt similarity index 90% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/TokenSelectorContentPreviewProvider.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentPreviewProvider.kt index 2a1922d067..d6301236d5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/TokenSelectorContentPreviewProvider.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentPreviewProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.preview +package com.tangem.common.ui.markets.tokenselector import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R @@ -8,15 +8,10 @@ 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 : +class TokenSelectorContentPreviewProvider : CollectionPreviewParameterProvider( listOf( tokenSelectorPreviewSimple(), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/TokenSelectorUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentUM.kt similarity index 80% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/TokenSelectorUM.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentUM.kt index 3268d7d2c8..55994b5a72 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/TokenSelectorUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorContentUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.state +package com.tangem.common.ui.markets.tokenselector import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -7,18 +7,18 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList @Immutable -internal data class TokenSelectorContentUM( +data class TokenSelectorContentUM( val sections: ImmutableList, ) : TangemBottomSheetConfigContent @Immutable -internal data class AccountHeaderData( +data class AccountHeaderData( val accountName: TextReference, val cryptoPortfolioIcon: CryptoPortfolioIcon, ) @Immutable -internal sealed interface TokenSelectorSectionUM { +sealed interface TokenSelectorSectionUM { data class WalletHeader(val walletName: String) : TokenSelectorSectionUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorList.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorList.kt similarity index 94% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorList.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorList.kt index 1e96099720..eddbe4fc8e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/TokenSelectorList.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorList.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.search.components +package com.tangem.common.ui.markets.tokenselector import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -21,11 +21,9 @@ 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) { +fun LazyListScope.tokenSelectorSectionItems(sections: ImmutableList) { sections.forEachIndexed { index, section -> when (section) { is TokenSelectorSectionUM.WalletHeader -> { diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/UserAssetItemUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/UserAssetItemUM.kt new file mode 100644 index 0000000000..1729f70d6c --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/UserAssetItemUM.kt @@ -0,0 +1,61 @@ +package com.tangem.common.ui.markets.tokenselector + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference + +@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 +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/portfolio/UserAssetEntry.kt similarity index 87% rename from domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/portfolio/UserAssetEntry.kt index c6a3d45854..661f192008 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/portfolio/UserAssetEntry.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.search.model +package com.tangem.domain.models.portfolio import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName @@ -6,7 +6,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -data class UserAssetSearchEntry( +data class UserAssetEntry( val userWalletId: UserWalletId, val userWalletName: String, val accountId: AccountId, diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt index 7a63289486..86387af79a 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt @@ -1,13 +1,15 @@ package com.tangem.domain.search.model +import com.tangem.domain.models.portfolio.UserAssetEntry + sealed interface UserAssetSearchItem { - data class Single(val entry: UserAssetSearchEntry) : UserAssetSearchItem + data class Single(val entry: UserAssetEntry) : UserAssetSearchItem data class Grouped( val tokenName: String, val tokenSymbol: String, val tokenIconUrl: String?, - val entries: List, + val entries: List, ) : UserAssetSearchItem } \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt index 0b6b37be48..ba5da04383 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.search.model.SearchResult -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.repository.SearchRepository import kotlinx.coroutines.flow.Flow @@ -97,7 +97,7 @@ class GetSearchResultsUseCase( return totalAccounts > 1 } - private fun groupAndSort(entries: List, shouldGroup: Boolean): List { + private fun groupAndSort(entries: List, shouldGroup: Boolean): List { if (!shouldGroup) { return entries .map { UserAssetSearchItem.Single(it) } @@ -129,7 +129,7 @@ class GetSearchResultsUseCase( statusList: AccountStatusList, wallets: Map, lowerQuery: String, - ): List { + ): List { val wallet = wallets[statusList.userWalletId] ?: return emptyList() return statusList.accountStatuses .filterCryptoPortfolio() @@ -141,7 +141,7 @@ class GetSearchResultsUseCase( name.contains(lowerQuery) || symbol.contains(lowerQuery) } .map { currencyStatus -> - UserAssetSearchEntry( + UserAssetEntry( userWalletId = statusList.userWalletId, userWalletName = wallet.name, accountId = accountStatus.accountId, diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt index 2261f0335d..d68a5f6d19 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt @@ -5,9 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent interface AddToPortfolioComponent : ComposableBottomSheetComponent { - data class Params( - val addToPortfolioManager: AddToPortfolioManager, - ) + data class Params(val addToPortfolioManager: AddToPortfolioManager) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt index e1d58b5033..70e41a3756 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt @@ -3,6 +3,7 @@ package com.tangem.features.commonfeatures.api.addtoportfolio import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher @@ -18,6 +19,7 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal { val onDismiss: Channel val onSuccessAdded: Channel + val onAddedTokenClick: Channel val portfolioFetcher: PortfolioFetcher val state: StateFlow @@ -27,36 +29,34 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal { sealed interface State { data object Loading : State - data class Ready( - val availableToAddData: AvailableToAddData, - ) : State { + data class Ready(val availableToAddData: AvailableToAddData) : State { val isAvailableToAdd: Boolean get() = availableToAddData.isAvailableToAdd val isSinglePortfolio: Boolean get() = availableToAddData.isSinglePortfolio } } @Serializable - data class AnalyticsParams( - val source: String?, - ) + data class AnalyticsParams(val source: String?) interface Factory { fun create(scope: CoroutineScope, settings: Settings, analyticsParams: AnalyticsParams): AddToPortfolioManager } + sealed interface LaunchMode { + data object DirectAdd : LaunchMode + data class ViaUserPortfolio(val rawCurrencyId: CryptoCurrency.RawID) : LaunchMode + } + /** * Immutable settings */ data class Settings( val shouldSkipTokenActionsScreen: Boolean = false, + val launchMode: LaunchMode = LaunchMode.DirectAdd, ) { companion object { - val DefaultMarket = Settings( - shouldSkipTokenActionsScreen = false, - ) - val ChooseToken = Settings( - shouldSkipTokenActionsScreen = true, - ) + val DefaultMarket = Settings(shouldSkipTokenActionsScreen = false) + val ChooseToken = Settings(shouldSkipTokenActionsScreen = true) } } @@ -64,10 +64,7 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal { * Mutable parameters * Updates may trigger reload [State] */ - data class Params( - val networks: List, - val token: TokenMarketParams, - ) + data class Params(val networks: List, val token: TokenMarketParams) data class Result( val wallet: UserWallet, @@ -88,4 +85,5 @@ interface AddToPortfolioManagerInternal { fun onDismiss() fun onSuccessAdded(result: AddToPortfolioManager.Result) + fun onAddedTokenClick(result: AddToPortfolioManager.Result) } \ No newline at end of file diff --git a/features/common-features/impl/build.gradle.kts b/features/common-features/impl/build.gradle.kts index afa9fcfb58..4632310a53 100644 --- a/features/common-features/impl/build.gradle.kts +++ b/features/common-features/impl/build.gradle.kts @@ -11,6 +11,10 @@ android { namespace = "com.tangem.features.commonfeatures.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Api */ implementation(projects.features.commonFeatures.api) @@ -56,6 +60,9 @@ dependencies { implementation(projects.common.uiMarkets) implementation(projects.common.routing) + /** Libs */ + implementation(projects.libs.blockchainSdk) + /** AndroidX libraries */ implementation(deps.androidx.core.ktx) implementation(deps.lifecycle.runtime.ktx) @@ -79,4 +86,9 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(projects.common.test) + testImplementation(projects.test.core) + testImplementation(projects.test.mock) } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt index 0a20db5947..3f2288d1f4 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.tangem.features.commonfeatures.impl.R import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -21,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent +import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes @Composable @@ -63,7 +63,9 @@ internal fun AddToPortfolioBottomSheet( bottom = 16.dp, ) val isScrollableContent = when (animatedStack.active.configuration) { - AddToPortfolioRoutes.PortfolioSelector -> false + AddToPortfolioRoutes.PortfolioSelector, + AddToPortfolioRoutes.UserPortfolio, + -> false AddToPortfolioRoutes.AddToken, AddToPortfolioRoutes.Empty, is AddToPortfolioRoutes.NetworkSelector, @@ -95,6 +97,7 @@ private fun AddToPortfolioBottomSheetTitle( AddToPortfolioRoutes.Empty -> TextReference.EMPTY is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network) AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token) + AddToPortfolioRoutes.UserPortfolio -> resourceReference(R.string.markets_portfolio_block_title) AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent) .title.collectAsStateWithLifecycle().value } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt index d69a90e75d..214ba365f5 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt @@ -11,14 +11,17 @@ 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.decompose.ComposableContentComponent -import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +@Suppress("LongParameterList") internal class DefaultAddToPortfolioComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: AddToPortfolioComponent.Params, @@ -26,6 +29,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor( addTokenComponentFactory: AddTokenComponent.Factory, tokenActionsComponentFactory: TokenActionsComponent.Factory, private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory, + private val userPortfolioComponentFactory: UserPortfolioComponent.Factory, ) : AppComponentContext by context, AddToPortfolioComponent { private val model: AddToPortfolioModel = getOrCreateModel(params) @@ -91,6 +95,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor( AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent AddToPortfolioRoutes.TokenActions -> tokenActionsComponent AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY + AddToPortfolioRoutes.UserPortfolio -> createUserPortfolioComponent(componentContext) is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create( context = childByContext(componentContext), params = ChooseNetworkComponent.Params( @@ -100,6 +105,19 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor( ) } + private fun createUserPortfolioComponent(componentContext: ComponentContext): ComposableContentComponent { + return when (model.addToPortfolioManager.settings.launchMode) { + AddToPortfolioManager.LaunchMode.DirectAdd -> ComposableContentComponent.EMPTY + is AddToPortfolioManager.LaunchMode.ViaUserPortfolio -> userPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = UserPortfolioComponent.Params( + uiState = model.userPortfolioStateController.uiState, + callbacks = model, + ), + ) + } + } + @AssistedFactory interface Factory : AddToPortfolioComponent.Factory { override fun create( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt index 7ba255d6e8..bb72f99975 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt @@ -79,6 +79,7 @@ internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject con AddToPortfolioRoutes.TokenActions -> ComposableContentComponent.EMPTY AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY is AddToPortfolioRoutes.NetworkSelector -> ComposableContentComponent.EMPTY + AddToPortfolioRoutes.UserPortfolio -> ComposableContentComponent.EMPTY } @AssistedFactory diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt index 871d7f7c44..c32baf5b2f 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt @@ -6,6 +6,8 @@ import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPrese import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioPreselectedDataComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.DefaultAddToPortfolioManager +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.DefaultUserPortfolioComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -25,4 +27,9 @@ internal interface AddToPortfolioComponentModule { fun bindAddToPortfolioPreselectedDataComponent( factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, ): AddToPortfolioPreselectedDataComponent.Factory + + @Binds + fun bindUserPortfolioComponentFactory( + factory: DefaultUserPortfolioComponent.Factory, + ): UserPortfolioComponent.Factory } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt index d0e2845d69..a0ab0d1bf4 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt @@ -7,6 +7,7 @@ import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfol import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -41,4 +42,9 @@ internal interface AddToPortfolioModelModule { @IntoMap @ClassKey(ChooseNetworkModel::class) fun chooseNetworkModel(model: ChooseNetworkModel): Model + + @Binds + @IntoMap + @ClassKey(UserPortfolioModel::class) + fun userPortfolioModel(model: UserPortfolioModel): Model } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt new file mode 100644 index 0000000000..8db0647303 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt @@ -0,0 +1,114 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.model + +import arrow.core.getOrElse +import com.tangem.domain.markets.GetTokenMarketCryptoCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddAccount +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet +import javax.inject.Inject + +internal class AddToPortfolioInitialSelectionResolver @Inject constructor( + private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, + private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, +) { + + suspend fun resolve( + availableToAddData: AvailableToAddData, + orderedNetworks: List, + selectedWallet: UserWallet?, + tokenParams: TokenMarketParams, + accountToAdd: AvailableToAddAccount? = null, + ): InitialSelection? { + if (availableToAddData.availableToAddWallets.isEmpty()) return null + val fallbackNetwork = orderedNetworks.firstOrNull() ?: return null + + val walletOrder = orderedWallets(availableToAddData, selectedWallet) + + if (accountToAdd != null) { + val ownerWallet = walletOrder.firstOrNull { entry -> + entry.availableToAddAccounts.values.any { it === accountToAdd } + } ?: walletOrder.first() + val network = pickAddableNetwork( + userWallet = ownerWallet.userWallet, + account = accountToAdd, + orderedNetworks = orderedNetworks, + tokenParams = tokenParams, + ) + ?: fallbackNetwork + return InitialSelection(userWallet = ownerWallet.userWallet, account = accountToAdd, network = network) + } + + for (walletEntry in walletOrder) { + val account = pickAvailableAccount(walletEntry) ?: continue + val network = pickAddableNetwork( + userWallet = walletEntry.userWallet, + account = account, + orderedNetworks = orderedNetworks, + tokenParams = tokenParams, + ) ?: continue + return InitialSelection(walletEntry.userWallet, account, network) + } + + val fallbackWallet = walletOrder.first() + val fallbackAccount = pickFallbackAccount(fallbackWallet) ?: return null + return InitialSelection(fallbackWallet.userWallet, fallbackAccount, fallbackNetwork) + } + + private fun orderedWallets(data: AvailableToAddData, selectedWallet: UserWallet?): List { + val preferred = selectedWallet?.walletId?.let { data.availableToAddWallets[it] } + return buildList { + preferred?.let(::add) + data.availableToAddWallets.values.forEach { entry -> + if (entry !== preferred) add(entry) + } + } + } + + private fun pickAvailableAccount(walletEntry: AvailableToAddWallet): AvailableToAddAccount? { + val mainId = AccountId.forMainCryptoPortfolio(walletEntry.userWallet.walletId) + return walletEntry.availableToAddAccounts[mainId]?.takeIf { it.isAvailableToAdd } + ?: walletEntry.availableToAddAccounts.values.firstOrNull { it.isAvailableToAdd } + } + + private fun pickFallbackAccount(walletEntry: AvailableToAddWallet): AvailableToAddAccount? { + val mainId = AccountId.forMainCryptoPortfolio(walletEntry.userWallet.walletId) + return walletEntry.availableToAddAccounts[mainId] + ?: walletEntry.availableToAddAccounts.values.firstOrNull() + } + + private suspend fun pickAddableNetwork( + userWallet: UserWallet, + account: AvailableToAddAccount, + orderedNetworks: List, + tokenParams: TokenMarketParams, + ): TokenMarketInfo.Network? { + val availableOrdered = orderedNetworks.filter { candidate -> + account.availableToAddNetworks.any { it.networkId == candidate.networkId } + } + if (availableOrdered.isEmpty()) return null + + val derivationIndex = account.account.account.derivationIndex + val withDerivation = availableOrdered.filter { network -> + val currency = getTokenMarketCryptoCurrency( + userWalletId = userWallet.walletId, + tokenMarketParams = tokenParams, + network = network, + accountIndex = derivationIndex, + ) ?: return@filter false + networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false } + } + + return withDerivation.firstOrNull() ?: availableOrdered.first() + } + + data class InitialSelection( + val userWallet: UserWallet, + val account: AvailableToAddAccount, + val network: TokenMarketInfo.Network, + ) +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index 18071b731c..3749d47c9d 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.popToFirst import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.common.ui.markets.action.CryptoCurrencyData import com.tangem.common.ui.markets.action.QuickActionsConverter.toQuickActions import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -11,22 +12,29 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.markets.GetTokenMarketCryptoCurrency import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.* import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController -import com.tangem.features.commonfeatures.api.addtoportfolio.* import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.ChooseNetworkComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state.UserPortfolioStateController import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job @@ -40,20 +48,25 @@ import javax.inject.Inject private const val TOKEN_ACTIONS_DELAY = 500L @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class AddToPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, + designFeatureToggles: DesignFeatureToggles, override val dispatchers: CoroutineDispatcherProvider, + val portfolioSelectorController: PortfolioSelectorController, private val callbackDelegate: AddToPortfolioCallbackDelegate, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, - val portfolioSelectorController: PortfolioSelectorController, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val selectionResolver: AddToPortfolioInitialSelectionResolver, + userPortfolioStateControllerFactory: UserPortfolioStateController.Factory, ) : Model(), ChooseNetworkComponent.Callbacks by callbackDelegate, TokenActionsComponent.Callbacks by callbackDelegate, - AddTokenComponent.Callbacks by callbackDelegate { + AddTokenComponent.Callbacks by callbackDelegate, + UserPortfolioComponent.Callbacks by callbackDelegate { private val params = paramsContainer.require() val navigation = StackNavigation() @@ -68,11 +81,24 @@ internal class AddToPortfolioModel @Inject constructor( val portfolioFetcher: PortfolioFetcher = addToPortfolioManager.portfolioFetcher val eventBuilder: MutableSharedFlow = replayMutableSharedFlow() + val userPortfolioStateController = userPortfolioStateControllerFactory.create( + modelScope = modelScope, + onTokenSelected = { result -> addToPortfolioManager.onAddedTokenClick(result) }, + ) + val featureData: Flow = combineFeatureData() + private val globalSelectedWallet: UserWallet? + get() = getSelectedWalletSyncUseCase().getOrNull() + .takeIf { it?.isMultiCurrency == true } + init { navigation.subscribe { currentStack = it.transformer.invoke(currentStack) } - startAddToPortfolioFlow() + if (designFeatureToggles.isRedesignEnabled) { + startRedesignAddToPortfolioFlow() + } else { + startLegacyAddToPortfolioFlow() + } } private fun replayMutableSharedFlow() = MutableSharedFlow( @@ -81,7 +107,7 @@ internal class AddToPortfolioModel @Inject constructor( ) @Suppress("LongMethod") - private fun startAddToPortfolioFlow() { + private fun startLegacyAddToPortfolioFlow() { channelFlow { fun finishSuccessFlow(result: AddToPortfolioManager.Result) { addToPortfolioManager.onSuccessAdded(result) @@ -208,6 +234,130 @@ internal class AddToPortfolioModel @Inject constructor( .launchIn(modelScope) } + @Suppress("LongMethod") + private fun startRedesignAddToPortfolioFlow() { + channelFlow { + fun finishSuccessFlow(result: AddToPortfolioManager.Result) { + addToPortfolioManager.onSuccessAdded(result) + channel.close() + } + + fun finishDismissFlow() { + addToPortfolioManager.onDismiss() + channel.close() + } + + val tokenMarketParams = addToPortfolioManager.paramsFlow.first().token + val eb = PortfolioAnalyticsEvent.EventBuilder( + tokenSymbol = tokenMarketParams.symbol, + source = addToPortfolioManager.analyticsParams.source, + ) + eventBuilder.tryEmit(eb) + + val launchMode = addToPortfolioManager.settings.launchMode + val initialData = featureData + .filterIsInstance() + .map { it.availableToAddData } + .first() + + if (launchMode is AddToPortfolioManager.LaunchMode.ViaUserPortfolio && + initialData.hasAnyAddedCurrency(launchMode.rawCurrencyId) + ) { + // suspend, must prepare UM before navigate to UserPortfolio + userPortfolioStateController.updateAndWaitNotNullState(initialData, launchMode.rawCurrencyId) + navigation.replaceAll(AddToPortfolioRoutes.UserPortfolio) + callbackDelegate.onContinueFromUserPortfolio.receiveAsFlow().first() + } + + val paramsSnapshot = addToPortfolioManager.paramsFlow.first() + val selection = selectionResolver.resolve( + availableToAddData = initialData, + orderedNetworks = paramsSnapshot.networks, + selectedWallet = globalSelectedWallet, + tokenParams = tokenMarketParams, + ) ?: run { + finishDismissFlow() + return@channelFlow + } + + val isAccountMode = portfolioSelectorController.isAccountModeSync() + portfolioSelectorController.selectAccount(selection.account.account.accountId) + + val firstSelectedPortfolio = SelectedPortfolio( + isAccountMode = isAccountMode, + userWallet = selection.userWallet, + account = selection.account, + isAvailableMorePortfolio = !initialData.isSinglePortfolio, + ) + val firstSelectedNetwork = selection.toSelectedNetwork() ?: run { + finishDismissFlow() + return@channelFlow + } + + selectedPortfolio.emit(firstSelectedPortfolio) + selectedNetwork.emit(firstSelectedNetwork) + + analyticsEventHandler.send(event = eventBuilder.first().popupToConfirm()) + navigation.replaceAll(AddToPortfolioRoutes.AddToken) + + var middleNavigationJob: Job? = null + callbackDelegate.onChangeNetworkClick.receiveAsFlow() + .onEach { + middleNavigationJob?.cancel() + middleNavigationJob = changeNetworkNavigationFlow() + .launchIn(this) + val route = routeToNetworkSelector(selectedPortfolio.first()) + navigation.pushNew(route) + } + .launchIn(this) + + callbackDelegate.onChangePortfolioClick.receiveAsFlow() + .onEach { + middleNavigationJob?.cancel() + middleNavigationJob = changePortfolioNavigationNewFlow( + data = initialData, + orderedNetworks = paramsSnapshot.networks, + tokenParams = tokenMarketParams, + ).launchIn(this) + logAccountSelector(isAccountMode) + navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) + } + .launchIn(this) + + val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() + middleNavigationJob?.cancel() + val selectedPortfolioSnapshot = selectedPortfolio.first() + val result = AddToPortfolioManager.Result( + wallet = selectedPortfolioSnapshot.userWallet, + account = selectedPortfolioSnapshot.account.account, + addedCurrency = addedToken, + ) + + messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) + + if (addToPortfolioManager.settings.shouldSkipTokenActionsScreen) { + finishSuccessFlow(result) + return@channelFlow + } + + setupTokenActionsFlow(selectedPortfolioSnapshot, addedToken) + .onEach { cryptoCurrencyData -> + tokenActionsData.emit(cryptoCurrencyData) + navigation.replaceAll(AddToPortfolioRoutes.TokenActions) + } + .onEmpty { finishSuccessFlow(result) } + .launchIn(this) + + callbackDelegate.onLaterClick.receiveAsFlow().first() + finishSuccessFlow(result) + } + .catch { throwable -> + TangemLogger.e("Error", throwable) + addToPortfolioManager.onDismiss() + } + .launchIn(modelScope) + } + private suspend fun logAccountSelector(isAccountMode: Boolean) { if (isAccountMode) { analyticsEventHandler.send(eventBuilder.first().popupToChooseAccount()) @@ -249,6 +399,38 @@ internal class AddToPortfolioModel @Inject constructor( ).collect { emit(it) } } + private fun changePortfolioNavigationNewFlow( + data: AvailableToAddData, + orderedNetworks: List, + tokenParams: TokenMarketParams, + ): Flow { + return setupPortfolioFlow(data) + // drop first selected portfolio or any selected before + .drop(1) + .map { newPortfolio -> + val currentNetwork = selectedNetwork.first().selectedNetwork + val availableToAddNetworks = newPortfolio.account.availableToAddNetworks + val isSelectedNetworkAvailableForNewPortfolio = availableToAddNetworks + .any { it.networkId == currentNetwork.networkId } + + if (!isSelectedNetworkAvailableForNewPortfolio) { + val selection = selectionResolver.resolve( + availableToAddData = data, + orderedNetworks = orderedNetworks, + selectedWallet = globalSelectedWallet, + tokenParams = tokenParams, + accountToAdd = newPortfolio.account, + ) + val newNetwork = selection?.toSelectedNetwork() + if (newNetwork != null) { + this.selectedNetwork.tryEmit(newNetwork) + } + } + selectedPortfolio.tryEmit(newPortfolio) + navigation.popToFirst() + } + } + private fun setupTokenActionsFlow( selectedPortfolio: SelectedPortfolio, addedToken: CryptoCurrencyStatus, @@ -350,19 +532,34 @@ internal class AddToPortfolioModel @Inject constructor( -> Unit } } + + private suspend fun AddToPortfolioInitialSelectionResolver.InitialSelection.toSelectedNetwork(): SelectedNetwork? { + val crypto = createCryptoCurrency( + userWallet = userWallet, + network = network, + account = account, + ) ?: return null + return SelectedNetwork( + cryptoCurrency = crypto, + selectedNetwork = network, + isAvailableMoreNetwork = !account.isSingleNetwork, + ) + } } @ModelScoped internal class AddToPortfolioCallbackDelegate @Inject constructor() : ChooseNetworkComponent.Callbacks, TokenActionsComponent.Callbacks, - AddTokenComponent.Callbacks { + AddTokenComponent.Callbacks, + UserPortfolioComponent.Callbacks { val onNetworkSelected = Channel() val onLaterClick = Channel() val onChangeNetworkClick = Channel() val onChangePortfolioClick = Channel() val onTokenAdded = Channel() + val onContinueFromUserPortfolio = Channel() override fun onNetworkSelected(network: TokenMarketInfo.Network) { onNetworkSelected.trySend(network) @@ -383,4 +580,19 @@ internal class AddToPortfolioCallbackDelegate @Inject constructor() : override fun onTokenAdded(status: CryptoCurrencyStatus) { onTokenAdded.trySend(status) } + + override fun onContinueFromUserPortfolio() { + onContinueFromUserPortfolio.trySend(Unit) + } +} + +private fun AvailableToAddData.hasAnyAddedCurrency(rawCurrencyId: CryptoCurrency.RawID): Boolean { + return availableToAddWallets.values.any { wallet -> + wallet.accounts.filterCryptoPortfolio().any { accountStatus -> + accountStatus.tokenList.flattenCurrencies().any { status -> + val id = status.currency.id.rawCurrencyId ?: return@any false + getTokenIdIfL2Network(id.value) == rawCurrencyId.value + } + } + } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt index e548f5b16f..8c01391c47 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt @@ -23,6 +23,9 @@ internal sealed interface AddToPortfolioRoutes : Route { @Serializable data object AddToken : AddToPortfolioRoutes + @Serializable + data object UserPortfolio : AddToPortfolioRoutes + @Serializable data object TokenActions : AddToPortfolioRoutes } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt index e0f328c406..7eb7d23e9c 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt @@ -26,6 +26,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor( override val onDismiss: Channel = Channel() override val onSuccessAdded: Channel = Channel() + override val onAddedTokenClick: Channel = Channel() override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), @@ -63,6 +64,10 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor( onSuccessAdded.trySend(result) } + override fun onAddedTokenClick(result: AddToPortfolioManager.Result) { + onAddedTokenClick.trySend(result) + } + override fun setTokenNetworks(networks: List) { updateInternal(networks = networks) } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt new file mode 100644 index 0000000000..d09e0cfcaf --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt @@ -0,0 +1,47 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.markets.tokenselector.StickyFooter +import com.tangem.common.ui.markets.tokenselector.TokenSelectorEmbeddedContent +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultUserPortfolioComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: UserPortfolioComponent.Params, +) : UserPortfolioComponent, AppComponentContext by context { + + private val model: UserPortfolioModel = getOrCreateModel(params) + private val onContinueClick: () -> Unit = params.callbacks::onContinueFromUserPortfolio + + @Composable + override fun Content(modifier: Modifier) { + val stateFlow = model.state.collectAsStateWithLifecycle() + val state = stateFlow.value ?: return + TokenSelectorEmbeddedContent( + content = state.content, + modifier = modifier, + stickyFooter = StickyFooter( + buttonText = resourceReference(R.string.common_add), + isEnabled = state.isAddEnabled, + onClick = onContinueClick, + ), + ) + } + + @AssistedFactory + interface Factory : UserPortfolioComponent.Factory { + override fun create( + context: AppComponentContext, + params: UserPortfolioComponent.Params, + ): DefaultUserPortfolioComponent + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/UserPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/UserPortfolioComponent.kt new file mode 100644 index 0000000000..b827f3568d --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/UserPortfolioComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM +import kotlinx.coroutines.flow.StateFlow + +internal interface UserPortfolioComponent : ComposableContentComponent { + + data class Params( + val uiState: StateFlow, + val callbacks: Callbacks, + ) + + interface Callbacks { + fun onContinueFromUserPortfolio() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioModel.kt new file mode 100644 index 0000000000..9bd839e9b9 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioModel.kt @@ -0,0 +1,20 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model + +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.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class UserPortfolioModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + val state: StateFlow = params.uiState +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioUM.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioUM.kt new file mode 100644 index 0000000000..bece060837 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/model/UserPortfolioUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM + +@Immutable +internal data class UserPortfolioUM( + val content: TokenSelectorContentUM, + val isAddEnabled: Boolean, +) \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/state/UserPortfolioStateController.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/state/UserPortfolioStateController.kt new file mode 100644 index 0000000000..2feeec1c92 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/state/UserPortfolioStateController.kt @@ -0,0 +1,57 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state + +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer.UserPortfolioSectionsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* + +internal class UserPortfolioStateController @AssistedInject constructor( + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + @Assisted private val modelScope: CoroutineScope, + @Assisted private val onTokenSelected: (AddToPortfolioManager.Result) -> Unit, +) { + + private val requiredDataFlow = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + val uiState: StateFlow = combine( + flow = requiredDataFlow.distinctUntilChanged(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), + ) { (allAvailableData, rawCurrencyId), appCurrency, isBalanceHidden -> + UserPortfolioSectionsTransformer( + availableData = allAvailableData, + rawCurrencyId = rawCurrencyId, + appCurrency = appCurrency, + isBalanceHidden = isBalanceHidden, + onTokenSelected = onTokenSelected, + ).transform() + } + .distinctUntilChanged() + .stateIn(modelScope, SharingStarted.Lazily, null) + + suspend fun updateAndWaitNotNullState(allAvailableData: AvailableToAddData, rawCurrencyId: CryptoCurrency.RawID) { + requiredDataFlow.tryEmit(allAvailableData to rawCurrencyId) + uiState.filterNotNull().firstOrNull() + } + + @AssistedFactory + interface Factory { + fun create( + modelScope: CoroutineScope, + onTokenSelected: (AddToPortfolioManager.Result) -> Unit, + ): UserPortfolioStateController + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/transformer/UserPortfolioSectionsTransformer.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/transformer/UserPortfolioSectionsTransformer.kt new file mode 100644 index 0000000000..1258e4fad7 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/transformer/UserPortfolioSectionsTransformer.kt @@ -0,0 +1,187 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer + +import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network +import com.tangem.common.ui.account.toUM +import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.common.ui.markets.tokenselector.* +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.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +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.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +internal class UserPortfolioSectionsTransformer( + private val availableData: AvailableToAddData, + private val rawCurrencyId: CryptoCurrency.RawID, + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, + private val onTokenSelected: (AddToPortfolioManager.Result) -> Unit, +) { + + private val iconConverter = CryptoCurrencyToIconStateConverter() + + fun transform(): UserPortfolioUM { + return UserPortfolioUM( + content = TokenSelectorContentUM( + sections = buildSections(entries = generateEntries(availableData)).toImmutableList(), + ), + isAddEnabled = availableData.isAvailableToAdd, + ) + } + + private fun generateEntries(data: AvailableToAddData): List { + return data.availableToAddWallets.values.flatMap { wallet -> + wallet.accounts.filterCryptoPortfolio().flatMap { accountStatus -> + accountStatus.tokenList.flattenCurrencies() + .filter { status -> status.currency.matchesRawId(rawCurrencyId) } + .map { status -> + PortfolioEntry( + wallet = wallet.userWallet, + account = accountStatus, + currencyStatus = status, + ) + } + } + } + } + + private fun buildSections(entries: List): List { + val sections = mutableListOf() + val byWallet = entries.groupBy { it.wallet.walletId } + val shouldShowWalletHeaders = byWallet.size > 1 + + for ((_, walletEntries) in byWallet) { + if (shouldShowWalletHeaders) { + sections.add( + TokenSelectorSectionUM.WalletHeader(walletName = walletEntries.first().wallet.name), + ) + } + + val byAccount = walletEntries.groupBy { it.account.account.accountId } + val shouldShowAccountHeaders = byAccount.size > 1 + + for ((_, accountEntries) in byAccount) { + val singles = accountEntries.map(::entryToSingle).toImmutableList() + val accountHeader = if (shouldShowAccountHeaders) { + val first = accountEntries.first() + AccountHeaderData( + accountName = first + .account + .account + .accountName + .toUM() + .value, + cryptoPortfolioIcon = first.account.account.icon, + ) + } else { + null + } + sections.add( + TokenSelectorSectionUM.TokenGroup(accountHeader = accountHeader, items = singles), + ) + } + } + return sections + } + + private fun entryToSingle(entry: PortfolioEntry): UserAssetItemUM.Single { + val currency = entry.currencyStatus.currency + val value = entry.currencyStatus.value + return UserAssetItemUM.Single( + id = "${entry.wallet.walletId.stringValue}_${entry.account.account.accountId.value}_${currency.id.value}", + icon = TangemIconUM.Currency(currencyIconState = iconConverter.convert(entry.currencyStatus)), + tokenName = currency.name, + tokenSymbol = currency.symbol, + fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) }, + priceChangeState = when (value) { + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAmount, + -> PriceChangeState.Unknown + else -> PriceChangeState.Content( + type = PriceChangeType.fromBigDecimal(value.priceChange.orZero()), + valueInPercent = value.priceChange.format { percent() }, + ) + }, + balanceState = convertBalanceState(value, currency.symbol, currency.decimals), + isBalanceHidden = isBalanceHidden, + onClick = { + onTokenSelected( + AddToPortfolioManager.Result( + wallet = entry.wallet, + account = entry.account, + addedCurrency = entry.currencyStatus, + ), + ) + }, + networkName = currency.network.name, + ) + } + + private data class PortfolioEntry( + val wallet: UserWallet, + val account: AccountStatus.CryptoPortfolio, + val currencyStatus: CryptoCurrencyStatus, + ) + + private fun convertBalanceState( + value: CryptoCurrencyStatus.Value, + symbol: String, + decimals: Int, + ): BalanceDisplayState { + return when { + value is CryptoCurrencyStatus.Loading && value.amount != null -> + BalanceDisplayState.Flickering( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = stringReference( + value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } + ?: StringsSigns.DASH_SIGN, + ), + ) + value is CryptoCurrencyStatus.Loading -> BalanceDisplayState.Loading + value is CryptoCurrencyStatus.Unreachable -> BalanceDisplayState.Unreachable + value.isError && value.amount != null -> + BalanceDisplayState.Stale( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = stringReference( + value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } + ?: StringsSigns.DASH_SIGN, + ), + ) + value.isError -> BalanceDisplayState.Unreachable + else -> BalanceDisplayState.Loaded( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = stringReference( + value.fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } + ?: StringsSigns.DASH_SIGN, + ), + ) + } + } + + private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String { + return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN + } + + private fun CryptoCurrency.matchesRawId(target: CryptoCurrency.RawID): Boolean { + val rawId = id.rawCurrencyId ?: return false + return getTokenIdIfL2Network(rawId.value) == target.value + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt new file mode 100644 index 0000000000..34c14e1224 --- /dev/null +++ b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt @@ -0,0 +1,591 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.model + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.domain.markets.GetTokenMarketCryptoCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddAccount +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AddToPortfolioInitialSelectionResolverTest { + + private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency = mockk() + private val networkHasDerivationUseCase: NetworkHasDerivationUseCase = mockk() + + private val tokenParams: TokenMarketParams = mockk() + + private lateinit var resolver: AddToPortfolioInitialSelectionResolver + + @BeforeEach + fun setup() { + clearMocks(getTokenMarketCryptoCurrency, networkHasDerivationUseCase) + coEvery { getTokenMarketCryptoCurrency(any(), any(), any(), any()) } returns null + every { networkHasDerivationUseCase(any(), any()) } returns false.right() + + resolver = AddToPortfolioInitialSelectionResolver( + getTokenMarketCryptoCurrency = getTokenMarketCryptoCurrency, + networkHasDerivationUseCase = networkHasDerivationUseCase, + ) + } + + @Test + fun `GIVEN no wallets in data WHEN resolve THEN return null`() = runTest { + val data = availableData(wallets = emptyMap()) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = null, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNull() + } + + @Test + fun `GIVEN accountToAdd is provided WHEN resolve THEN use it instead of looking up account`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val mainAccount = availableAccount() + val explicitAccount = availableAccount(availableToAddNetworks = setOf(BITCOIN)) + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to mainAccount), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount) + Truth.assertThat(result.network).isEqualTo(BITCOIN) + } + + @Test + fun `GIVEN accountToAdd is not available to add WHEN resolve THEN still use it`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val explicitAccount = availableAccount(isAvailableToAdd = false, availableToAddNetworks = setOf(ETHEREUM)) + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount) + } + + @Test + fun `GIVEN accountToAdd with no matching ordered networks WHEN resolve THEN fall back to first ordered network`() = + runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM)) + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.account).isSameInstanceAs(explicitAccount) + Truth.assertThat(result.network).isEqualTo(BITCOIN) + } + + @Test + fun `GIVEN selected wallet is in data WHEN resolve THEN pick its entry`() = runTest { + val selectedWalletId = UserWalletId(WALLET_ID_A) + val otherWalletId = UserWalletId(WALLET_ID_B) + val selectedUserWallet = userWallet(selectedWalletId) + val otherUserWallet = userWallet(otherWalletId) + + val selectedAccount = availableAccount() + val otherAccount = availableAccount() + + val data = availableData( + wallets = linkedMapOf( + otherWalletId to walletEntry(otherUserWallet, mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherAccount)), + selectedWalletId to walletEntry(selectedUserWallet, mapOf(AccountId.forMainCryptoPortfolio(selectedWalletId) to selectedAccount)), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = selectedUserWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(selectedUserWallet) + Truth.assertThat(result.account).isSameInstanceAs(selectedAccount) + } + + @Test + fun `GIVEN selected wallet is not in data WHEN resolve THEN fall back to first wallet`() = runTest { + val firstWalletId = UserWalletId(WALLET_ID_A) + val firstUserWallet = userWallet(firstWalletId) + val firstAccount = availableAccount() + + val data = availableData( + wallets = mapOf( + firstWalletId to walletEntry(firstUserWallet, mapOf(AccountId.forMainCryptoPortfolio(firstWalletId) to firstAccount)), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = userWallet(UserWalletId(WALLET_ID_B)), + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(firstUserWallet) + } + + @Test + fun `GIVEN selected wallet is null WHEN resolve THEN fall back to first wallet`() = runTest { + val firstWalletId = UserWalletId(WALLET_ID_A) + val firstUserWallet = userWallet(firstWalletId) + val firstAccount = availableAccount() + + val data = availableData( + wallets = mapOf( + firstWalletId to walletEntry(firstUserWallet, mapOf(AccountId.forMainCryptoPortfolio(firstWalletId) to firstAccount)), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = null, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(firstUserWallet) + } + + @Test + fun `GIVEN main account is available WHEN resolve THEN pick main account`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val mainAccount = availableAccount() + val otherAccount = availableAccount() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = linkedMapOf( + AccountId.forPaymentAccount(walletId) to otherAccount, + AccountId.forMainCryptoPortfolio(walletId) to mainAccount, + ), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.account).isSameInstanceAs(mainAccount) + } + + @Test + fun `GIVEN main account is not available WHEN resolve THEN pick first available account`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val mainAccount = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet()) + val secondAvailable = availableAccount() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = linkedMapOf( + AccountId.forMainCryptoPortfolio(walletId) to mainAccount, + AccountId.forPaymentAccount(walletId) to secondAvailable, + ), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.account).isSameInstanceAs(secondAvailable) + } + + @Test + fun `GIVEN no available accounts WHEN resolve THEN fall back to that account with first ordered network`() = + runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val unavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet()) + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to unavailable), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(userWallet) + Truth.assertThat(result.account).isSameInstanceAs(unavailable) + Truth.assertThat(result.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN ordered networks do not match account networks WHEN resolve THEN fall back to first ordered network`() = + runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM)) + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(userWallet) + Truth.assertThat(result.account).isSameInstanceAs(account) + Truth.assertThat(result.network).isEqualTo(BITCOIN) + } + + @Test + fun `GIVEN preferred wallet has no available accounts but another wallet does WHEN resolve THEN pick the other`() = + runTest { + val preferredWalletId = UserWalletId(WALLET_ID_A) + val otherWalletId = UserWalletId(WALLET_ID_B) + val preferredUserWallet = userWallet(preferredWalletId) + val otherUserWallet = userWallet(otherWalletId) + + val preferredUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet()) + val otherAvailable = availableAccount(availableToAddNetworks = setOf(ETHEREUM)) + + val data = availableData( + wallets = linkedMapOf( + preferredWalletId to walletEntry( + userWallet = preferredUserWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(preferredWalletId) to preferredUnavailable), + ), + otherWalletId to walletEntry( + userWallet = otherUserWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherAvailable), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = preferredUserWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(otherUserWallet) + Truth.assertThat(result.account).isSameInstanceAs(otherAvailable) + Truth.assertThat(result.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN no wallet has viable combo WHEN resolve THEN fall back to preferred wallet with first ordered network`() = + runTest { + val preferredWalletId = UserWalletId(WALLET_ID_A) + val otherWalletId = UserWalletId(WALLET_ID_B) + val preferredUserWallet = userWallet(preferredWalletId) + val otherUserWallet = userWallet(otherWalletId) + + val preferredUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet()) + val otherUnavailable = availableAccount(isAvailableToAdd = false, availableToAddNetworks = emptySet()) + + val data = availableData( + wallets = linkedMapOf( + otherWalletId to walletEntry( + userWallet = otherUserWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(otherWalletId) to otherUnavailable), + ), + preferredWalletId to walletEntry( + userWallet = preferredUserWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(preferredWalletId) to preferredUnavailable), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM), + selectedWallet = preferredUserWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result).isNotNull() + Truth.assertThat(result!!.userWallet).isSameInstanceAs(preferredUserWallet) + Truth.assertThat(result.account).isSameInstanceAs(preferredUnavailable) + Truth.assertThat(result.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN first ordered network has derivation WHEN resolve THEN pick first`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right() + every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM, BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result?.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN only second ordered network has derivation WHEN resolve THEN pick second`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns false.right() + every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM, BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result?.network).isEqualTo(BITCOIN) + } + + @Test + fun `GIVEN no network has derivation WHEN resolve THEN fall back to first ordered available network`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), any()) } returns Throwable().left() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN, ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result?.network).isEqualTo(BITCOIN) + } + + @Test + fun `GIVEN get token market crypto currency returns null WHEN resolve THEN fall back to first ordered available network`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val account = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + coEvery { getTokenMarketCryptoCurrency(any(), any(), any(), any()) } returns null + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to account), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN, ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + ) + + Truth.assertThat(result?.network).isEqualTo(BITCOIN) + } + + // region Helpers + + private fun availableData(wallets: Map): AvailableToAddData = mockk { + every { isAvailableToAdd } returns true + every { availableToAddWallets } returns wallets + } + + private fun walletEntry( + userWallet: UserWallet, + accounts: Map, + ): AvailableToAddWallet = mockk { + every { this@mockk.userWallet } returns userWallet + every { availableToAddAccounts } returns accounts + } + + private fun availableAccount( + isAvailableToAdd: Boolean = true, + availableToAddNetworks: Set = setOf(ETHEREUM), + derivationIndex: DerivationIndex = DerivationIndex.Main, + ): AvailableToAddAccount = mockk { + every { this@mockk.isAvailableToAdd } returns isAvailableToAdd + every { this@mockk.availableToAddNetworks } returns availableToAddNetworks + every { account.account.derivationIndex } returns derivationIndex + } + + private fun userWallet(walletId: UserWalletId): UserWallet = mockk { + every { this@mockk.walletId } returns walletId + } + + private fun cryptoCurrency(): CryptoCurrency = mockk { + every { network } returns mockk() + } + + // endregion + + private companion object { + const val WALLET_ID_A = "011f" + const val WALLET_ID_B = "022e" + + val ETHEREUM = TokenMarketInfo.Network( + networkId = "ethereum", + isExchangeable = true, + contractAddress = null, + decimalCount = 18, + ) + + val BITCOIN = TokenMarketInfo.Network( + networkId = "bitcoin", + isExchangeable = true, + contractAddress = null, + decimalCount = 8, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index e8f7af83ef..cb3ad4c829 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -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 -> { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/AddToPortfolioSlotRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/AddToPortfolioSlotRoute.kt new file mode 100644 index 0000000000..fc1cfda981 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/AddToPortfolioSlotRoute.kt @@ -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 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index c4ac5551aa..0e521d801a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -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) { 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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockComponent.kt index 22f52fccc7..b9d274b71b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockComponent.kt @@ -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) { @@ -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( - 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?, + ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockParentClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockParentClickIntents.kt new file mode 100644 index 0000000000..8be21c2648 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/PortfolioBlockParentClickIntents.kt @@ -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) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockModel.kt index 48901a9e3c..873efb8c79 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockModel.kt @@ -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 field = MutableStateFlow(PortfolioBlockUM.Loading) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val cryptoCurrencyIdState: StateFlow field = MutableStateFlow(null) - private val params = paramsContainer.require() + private val params = paramsContainer.require() 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, settings: SettingsBox): PortfolioBlockUM { + private fun buildState( + portfolios: List, + 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, -) - -private data class SettingsBox( - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockRoute.kt deleted file mode 100644 index 4c1f19107b..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/model/PortfolioBlockRoute.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt index 06cc01d383..e372da6cda 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt @@ -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 = {}, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/state/PortfolioBlockUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/state/PortfolioBlockUM.kt index f61b90aa05..136a952189 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/state/PortfolioBlockUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/state/PortfolioBlockUM.kt @@ -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() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchBottomSheetRoute.kt index 7ac509eccf..b595fbe70c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchBottomSheetRoute.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchBottomSheetRoute.kt @@ -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, + val entries: List, val appCurrency: AppCurrency, val isBalanceHidden: Boolean, - val onTokenSelected: (UserAssetSearchEntry) -> Unit, + val onTokenSelected: (UserAssetEntry) -> Unit, val onDismiss: () -> Unit, ) : SearchBottomSheetRoute } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchTokenSelectorComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchTokenSelectorComponent.kt index a36895267e..6fe668dc82 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchTokenSelectorComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/SearchTokenSelectorComponent.kt @@ -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, + val entries: List, val appCurrency: AppCurrency, val isBalanceHidden: Boolean, - val onTokenSelected: (UserAssetSearchEntry) -> Unit, + val onTokenSelected: (UserAssetEntry) -> Unit, val onDismiss: () -> Unit, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index 024e7ff952..2916584e41 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -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.Loading) + val addToPortfolioSheetNavigation = SlotNavigation() + + 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() { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index 8dc79c7ec2..28d4ac7e21 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -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) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchTokenSelectorModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchTokenSelectorModel.kt index 9c8a912394..7111311a4d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchTokenSelectorModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchTokenSelectorModel.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt index 9d9e043453..a57bd66a42 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt @@ -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 { @@ -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, + entries: List, symbol: String, decimals: Int, ): BalanceDisplayState { @@ -145,7 +145,7 @@ internal class UserAssetSearchItemConverter( } private fun computeGroupBalance( - entries: List, + entries: List, symbol: String, decimals: Int, ): BalanceDisplayState.Loaded { @@ -158,7 +158,7 @@ internal class UserAssetSearchItemConverter( } private fun computeGroupBalanceFlickering( - entries: List, + entries: List, symbol: String, decimals: Int, ): BalanceDisplayState.Flickering { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/TokenSelectorStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/TokenSelectorStateController.kt index 40effc11de..6e3c068eeb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/TokenSelectorStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/TokenSelectorStateController.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformer.kt index a6c2729c0e..c3d9e66e4d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformer.kt @@ -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, + private val entries: List, private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, - private val onTokenSelected: (UserAssetSearchEntry) -> Unit, + private val onTokenSelected: (UserAssetEntry) -> Unit, ) : TokenSelectorUMTransformer { private val entryConverter = TokenSelectorEntryConverter( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverter.kt index abc37b6790..7f6f59f367 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverter.kt @@ -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 { + private val onTokenSelected: (UserAssetEntry) -> Unit, +) : Converter { 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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorUMTransformer.kt index f364f03d07..480f20b319 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorUMTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorUMTransformer.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt index d99fdf0e7c..91e0fccbbe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt @@ -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( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index 33b2589a72..41bed10ea9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index 298dbfbcf1..24cc5d32c7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt index 2c9aecee9d..b2464602e1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt @@ -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( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt index 595a24e12b..7dbf384023 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt @@ -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 -} \ No newline at end of file +data class TextHintItemUM(val text: String) \ No newline at end of file diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformerTest.kt index 5d588ab0d3..dd22b5353c 100644 --- a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformerTest.kt +++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/BuildTokenSelectorSectionsTransformerTest.kt @@ -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): BuildTokenSelectorSectionsTransformer { + private fun createTransformer(entries: List): 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(relaxed = true) { every { name } returns "Network" } @@ -290,7 +290,7 @@ class BuildTokenSelectorSectionsTransformerTest { sources = CryptoCurrencyStatus.Sources(), ) } - return mockk { + return mockk { every { userWalletId } returns walletId every { userWalletName } returns walletName every { this@mockk.accountId } returns accountId diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverterTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverterTest.kt index 9663191563..579609ec26 100644 --- a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverterTest.kt +++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/search/state/transformers/TokenSelectorEntryConverterTest.kt @@ -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 { every { stringValue } returns walletId } @@ -302,7 +302,7 @@ class TokenSelectorEntryConverterTest { every { this@mockk.currency } returns currency every { this@mockk.value } returns value } - return mockk { + return mockk { every { this@mockk.userWalletId } returns userWalletId every { this@mockk.userWalletName } returns "Wallet" every { this@mockk.accountId } returns accountIdMock