Updated on 2026-08-14
This commit is contained in:
parent
3da4771d66
commit
3baed1e815
57 changed files with 1797 additions and 370 deletions
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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<TokenSelectorContentUM>(
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<TokenSelectorContentUM>(
|
||||
listOf(
|
||||
tokenSelectorPreviewSimple(),
|
||||
|
|
@ -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<TokenSelectorSectionUM>,
|
||||
) : 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
|
||||
|
||||
|
|
@ -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<TokenSelectorSectionUM>) {
|
||||
fun LazyListScope.tokenSelectorSectionItems(sections: ImmutableList<TokenSelectorSectionUM>) {
|
||||
sections.forEachIndexed { index, section ->
|
||||
when (section) {
|
||||
is TokenSelectorSectionUM.WalletHeader -> {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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<UserAssetSearchEntry>,
|
||||
val entries: List<UserAssetEntry>,
|
||||
) : UserAssetSearchItem
|
||||
}
|
||||
|
|
@ -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<UserAssetSearchEntry>, shouldGroup: Boolean): List<UserAssetSearchItem> {
|
||||
private fun groupAndSort(entries: List<UserAssetEntry>, shouldGroup: Boolean): List<UserAssetSearchItem> {
|
||||
if (!shouldGroup) {
|
||||
return entries
|
||||
.map { UserAssetSearchItem.Single(it) }
|
||||
|
|
@ -129,7 +129,7 @@ class GetSearchResultsUseCase(
|
|||
statusList: AccountStatusList,
|
||||
wallets: Map<UserWalletId, UserWallet>,
|
||||
lowerQuery: String,
|
||||
): List<UserAssetSearchEntry> {
|
||||
): List<UserAssetEntry> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<Params, AddToPortfolioComponent>
|
||||
}
|
||||
|
|
@ -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<Unit>
|
||||
val onSuccessAdded: Channel<Result>
|
||||
val onAddedTokenClick: Channel<Result>
|
||||
|
||||
val portfolioFetcher: PortfolioFetcher
|
||||
val state: StateFlow<State>
|
||||
|
|
@ -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<TokenMarketInfo.Network>,
|
||||
val token: TokenMarketParams,
|
||||
)
|
||||
data class Params(val networks: List<TokenMarketInfo.Network>, 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)
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ android {
|
|||
namespace = "com.tangem.features.commonfeatures.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<TokenMarketInfo.Network>,
|
||||
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<AvailableToAddWallet> {
|
||||
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<TokenMarketInfo.Network>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<AddToPortfolioComponent.Params>()
|
||||
val navigation = StackNavigation<AddToPortfolioRoutes>()
|
||||
|
|
@ -68,11 +81,24 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
val portfolioFetcher: PortfolioFetcher = addToPortfolioManager.portfolioFetcher
|
||||
val eventBuilder: MutableSharedFlow<PortfolioAnalyticsEvent.EventBuilder> = replayMutableSharedFlow()
|
||||
|
||||
val userPortfolioStateController = userPortfolioStateControllerFactory.create(
|
||||
modelScope = modelScope,
|
||||
onTokenSelected = { result -> addToPortfolioManager.onAddedTokenClick(result) },
|
||||
)
|
||||
|
||||
val featureData: Flow<AddToPortfolioManager.State> = 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 <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
|
||||
|
|
@ -81,7 +107,7 @@ internal class AddToPortfolioModel @Inject constructor(
|
|||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun startAddToPortfolioFlow() {
|
||||
private fun startLegacyAddToPortfolioFlow() {
|
||||
channelFlow<Unit> {
|
||||
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<Unit> {
|
||||
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<AddToPortfolioManager.State.Ready>()
|
||||
.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<TokenMarketInfo.Network>,
|
||||
tokenParams: TokenMarketParams,
|
||||
): Flow<Unit> {
|
||||
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<TokenMarketInfo.Network>()
|
||||
val onLaterClick = Channel<Unit>()
|
||||
val onChangeNetworkClick = Channel<Unit>()
|
||||
val onChangePortfolioClick = Channel<Unit>()
|
||||
val onTokenAdded = Channel<CryptoCurrencyStatus>()
|
||||
val onContinueFromUserPortfolio = Channel<Unit>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,9 @@ internal sealed interface AddToPortfolioRoutes : Route {
|
|||
@Serializable
|
||||
data object AddToken : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object UserPortfolio : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object TokenActions : AddToPortfolioRoutes
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
|
|||
|
||||
override val onDismiss: Channel<Unit> = Channel()
|
||||
override val onSuccessAdded: Channel<AddToPortfolioManager.Result> = Channel()
|
||||
override val onAddedTokenClick: Channel<AddToPortfolioManager.Result> = 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<TokenMarketInfo.Network>) {
|
||||
updateInternal(networks = networks)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserPortfolioUM?>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onContinueFromUserPortfolio()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, UserPortfolioComponent>
|
||||
}
|
||||
|
|
@ -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<UserPortfolioComponent.Params>()
|
||||
|
||||
val state: StateFlow<UserPortfolioUM?> = params.uiState
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<Pair<AvailableToAddData, CryptoCurrency.RawID>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<UserPortfolioUM?> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PortfolioEntry> {
|
||||
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<PortfolioEntry>): List<TokenSelectorSectionUM> {
|
||||
val sections = mutableListOf<TokenSelectorSectionUM>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWalletId, AvailableToAddWallet>): AvailableToAddData = mockk {
|
||||
every { isAvailableToAdd } returns true
|
||||
every { availableToAddWallets } returns wallets
|
||||
}
|
||||
|
||||
private fun walletEntry(
|
||||
userWallet: UserWallet,
|
||||
accounts: Map<AccountId, AvailableToAddAccount>,
|
||||
): AvailableToAddWallet = mockk {
|
||||
every { this@mockk.userWallet } returns userWallet
|
||||
every { availableToAddAccounts } returns accounts
|
||||
}
|
||||
|
||||
private fun availableAccount(
|
||||
isAvailableToAdd: Boolean = true,
|
||||
availableToAddNetworks: Set<TokenMarketInfo.Network> = 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<Network>()
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -16,16 +16,20 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.domain.markets.PreselectedTokenDetailsSection
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
|
|
@ -34,10 +38,13 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
|||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PreselectedTokenDetailsSection
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockParentClickIntents
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
|
|
@ -48,13 +55,15 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMarketsTokenDetailsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
val params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
designFeatureToggles: DesignFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
|
||||
val params: Params,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
// applying l2 compatibility
|
||||
|
|
@ -83,12 +92,28 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) {
|
||||
portfolioBlockComponentFactory.create(
|
||||
context = child("portfolio_block"),
|
||||
params = PortfolioBlockComponent.Params(updatedParams.token),
|
||||
params = PortfolioBlockComponent.Params(token = updatedParams.token),
|
||||
parentRouter = object : PortfolioBlockParentClickIntents {
|
||||
override fun openAddToPortfolioDirect() {
|
||||
model.openAddToPortfolio()
|
||||
}
|
||||
|
||||
override fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
|
||||
model.openAddToPortfolioViaUserPortfolio(rawCurrencyId)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val addToPortfolioSlot = childSlot(
|
||||
source = model.addToPortfolioSheetNavigation,
|
||||
serializer = AddToPortfolioSlotRoute.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::addToPortfolioChild,
|
||||
)
|
||||
|
||||
init {
|
||||
componentScope.launch(dispatchers.default) {
|
||||
model.networksState.collectLatest { state ->
|
||||
|
|
@ -120,6 +145,17 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addToPortfolioChild(
|
||||
@Suppress("UNUSED_PARAMETER") config: AddToPortfolioSlotRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent {
|
||||
val manager = model.addToPortfolioManagerOrNull() ?: return ComposableBottomSheetComponent.EMPTY
|
||||
return addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(addToPortfolioManager = manager),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
|
@ -198,6 +234,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
}
|
||||
}
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by addToPortfolioSlot.subscribeAsState()
|
||||
val bsState by bottomSheetState
|
||||
LaunchedEffect(bsState) {
|
||||
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
|
||||
|
|
@ -219,6 +256,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
}
|
||||
},
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -5,24 +5,13 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.PortfolioBlock
|
||||
import com.tangem.features.feed.components.portfolio.PortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -32,26 +21,14 @@ import kotlinx.serialization.Serializable
|
|||
internal class PortfolioBlockComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
@Assisted private val parentRouter: PortfolioBlockParentClickIntents?,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val portfolioComponentFactory: PortfolioComponent.Factory = object : PortfolioComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: PortfolioComponent.Params): PortfolioComponent {
|
||||
TODO("STUB. Will be implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
)
|
||||
|
||||
private val model: PortfolioBlockModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = PortfolioBlockRoute.serializer(),
|
||||
handleBackButton = true,
|
||||
childFactory = ::createBottomSheetChild,
|
||||
private val model: PortfolioBlockModel = getOrCreateModel(
|
||||
PortfolioBlockModelParams(
|
||||
token = params.token,
|
||||
parentRouter = parentRouter,
|
||||
),
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
|
|
@ -65,51 +42,23 @@ internal class PortfolioBlockComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
PortfolioBlock(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
private fun createBottomSheetChild(
|
||||
config: PortfolioBlockRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent {
|
||||
val currencyId = model.cryptoCurrencyIdState.value ?: return ComposableBottomSheetComponent.EMPTY
|
||||
val portfolioComponent = portfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = PortfolioComponent.Params(id = currencyId),
|
||||
)
|
||||
return PortfolioBottomSheetWrapper(
|
||||
portfolioComponent = portfolioComponent,
|
||||
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
private class PortfolioBottomSheetWrapper(
|
||||
private val portfolioComponent: PortfolioComponent,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : ComposableBottomSheetComponent {
|
||||
|
||||
override fun dismiss() = onDismiss()
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
) {
|
||||
portfolioComponent.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
data class Params(val token: TokenMarketParams)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): PortfolioBlockComponent
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: Params,
|
||||
parentRouter: PortfolioBlockParentClickIntents?,
|
||||
): PortfolioBlockComponent
|
||||
}
|
||||
|
||||
data class PortfolioBlockModelParams(
|
||||
val token: TokenMarketParams,
|
||||
val parentRouter: PortfolioBlockParentClickIntents?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package com.tangem.features.feed.components.market.details.portfolioblock.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.common.getTotalFiatAmount
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -26,6 +24,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockParentClickIntents
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -50,13 +49,12 @@ internal class PortfolioBlockModel @Inject constructor(
|
|||
val state: StateFlow<PortfolioBlockUM>
|
||||
field = MutableStateFlow<PortfolioBlockUM>(PortfolioBlockUM.Loading)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<PortfolioBlockRoute> = SlotNavigation()
|
||||
|
||||
val cryptoCurrencyIdState: StateFlow<CryptoCurrency.ID?>
|
||||
field = MutableStateFlow(null)
|
||||
|
||||
private val params = paramsContainer.require<PortfolioBlockComponent.Params>()
|
||||
private val params = paramsContainer.require<PortfolioBlockComponent.PortfolioBlockModelParams>()
|
||||
private val currencyRawId: CryptoCurrency.RawID = params.token.id
|
||||
private val parentRouter: PortfolioBlockParentClickIntents? = params.parentRouter
|
||||
|
||||
private val tokenIcon: CurrencyIconState = CurrencyIconState.CoinIcon(
|
||||
url = params.token.imageUrl,
|
||||
|
|
@ -114,15 +112,12 @@ internal class PortfolioBlockModel @Inject constructor(
|
|||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val settingsFlow = combine(
|
||||
return combine(
|
||||
portfolioDataFlow,
|
||||
getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { appCurrency, isBalanceHidden ->
|
||||
SettingsBox(appCurrency, isBalanceHidden)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
return combine(portfolioDataFlow, settingsFlow) { portfolios, settings ->
|
||||
buildState(portfolios, settings)
|
||||
) { portfolios, appCurrency, isBalanceHidden ->
|
||||
buildState(portfolios, appCurrency, isBalanceHidden)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +130,11 @@ internal class PortfolioBlockModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildState(portfolios: List<WalletPortfolio>, settings: SettingsBox): PortfolioBlockUM {
|
||||
private fun buildState(
|
||||
portfolios: List<WalletPortfolio>,
|
||||
appCurrency: AppCurrency,
|
||||
isBalanceHidden: Boolean,
|
||||
): PortfolioBlockUM {
|
||||
val allCurrencies = portfolios.flatMap { it.currencies }
|
||||
val hasMultiCurrencyWallet = portfolios.any { it.userWallet.isMultiCurrency }
|
||||
|
||||
|
|
@ -143,36 +142,36 @@ internal class PortfolioBlockModel @Inject constructor(
|
|||
return if (hasMultiCurrencyWallet) {
|
||||
PortfolioBlockUM.AddToken(
|
||||
tokenIcon = tokenIcon,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
onAddClick = { parentRouter?.openAddToPortfolioDirect() },
|
||||
)
|
||||
} else {
|
||||
PortfolioBlockUM.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
cryptoCurrencyIdState.update {
|
||||
it ?: allCurrencies.first().currency.id
|
||||
}
|
||||
cryptoCurrencyIdState.update { it ?: allCurrencies.first().currency.id }
|
||||
|
||||
val totalFiat = allCurrencies.mapNotNull { it.getTotalFiatAmount() }
|
||||
.fold(BigDecimal.ZERO, BigDecimal::add)
|
||||
|
||||
val formattedBalance = totalFiat.formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = settings.appCurrency.code,
|
||||
fiatCurrencySymbol = settings.appCurrency.symbol,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
}
|
||||
|
||||
val firstCurrency = allCurrencies.first().currency
|
||||
return PortfolioBlockUM.Content(
|
||||
totalBalance = formattedBalance,
|
||||
tokensInPortfolioCount = allCurrencies.size,
|
||||
tokenIcon = tokenIcon,
|
||||
tokenName = allCurrencies.first().currency.name,
|
||||
tokenSymbol = allCurrencies.first().currency.symbol,
|
||||
isBalanceHidden = settings.isBalanceHidden,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
tokenName = firstCurrency.name,
|
||||
tokenSymbol = firstCurrency.symbol,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onRowClick = { parentRouter?.openAddToPortfolioViaUserPortfolio(currencyRawId) },
|
||||
onAddFundsClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -180,9 +179,4 @@ internal class PortfolioBlockModel @Inject constructor(
|
|||
private data class WalletPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val currencies: List<CryptoCurrencyStatus>,
|
||||
)
|
||||
|
||||
private data class SettingsBox(
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
package com.tangem.features.feed.components.search
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
|
||||
internal sealed interface SearchBottomSheetRoute {
|
||||
|
||||
data class TokenSelector(
|
||||
val entries: List<UserAssetSearchEntry>,
|
||||
val entries: List<UserAssetEntry>,
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val onTokenSelected: (UserAssetSearchEntry) -> Unit,
|
||||
val onTokenSelected: (UserAssetEntry) -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
) : SearchBottomSheetRoute
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import com.tangem.core.decompose.model.getOrCreateModel
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorBottomSheet
|
||||
import com.tangem.features.feed.model.search.SearchTokenSelectorModel
|
||||
import com.tangem.features.feed.ui.search.components.TokenSelectorBottomSheet
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
|
|
@ -37,10 +37,10 @@ internal class SearchTokenSelectorComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
data class Params(
|
||||
val entries: List<UserAssetSearchEntry>,
|
||||
val entries: List<UserAssetEntry>,
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val onTokenSelected: (UserAssetSearchEntry) -> Unit,
|
||||
val onTokenSelected: (UserAssetEntry) -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ package com.tangem.features.feed.model.market.details
|
|||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.common.TangemSiteShareUrlBuilder
|
||||
import com.tangem.domain.markets.PreselectedTokenDetailsSection
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartDataProducer
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
|
|
@ -36,6 +40,7 @@ import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.domain.news.usecase.GetNewsUseCase
|
||||
|
|
@ -43,6 +48,8 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
|||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.feed.components.market.details.AddToPortfolioSlotRoute
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.details.analytics.MarketTokenAnalyticsEvent
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -65,6 +72,7 @@ import com.tangem.utils.coroutines.saveIn
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -81,6 +89,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
|
|
@ -93,6 +102,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val getNewsUseCase: GetNewsUseCase,
|
||||
private val shareManager: ShareManager,
|
||||
private val appRouter: AppRouter,
|
||||
) : Model() {
|
||||
|
||||
private val quotesJob = JobHolder()
|
||||
|
|
@ -224,6 +234,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
|
||||
|
||||
val addToPortfolioSheetNavigation = SlotNavigation<AddToPortfolioSlotRoute>()
|
||||
|
||||
private val isAddToPortfolioAvailable: Boolean =
|
||||
params.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled
|
||||
private var addToPortfolioManager: AddToPortfolioManager? = null
|
||||
private var addToPortfolioListenersJob: Job? = null
|
||||
|
||||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
|
|
@ -309,6 +326,77 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
onListedOnClick(exchangesCount)
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
networksState.collect { tokenNetworkState ->
|
||||
val manager = addToPortfolioManager ?: return@collect
|
||||
when (tokenNetworkState) {
|
||||
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(tokenNetworkState.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openAddToPortfolio() {
|
||||
if (!isAddToPortfolioAvailable) return
|
||||
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.DirectAdd)
|
||||
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
|
||||
}
|
||||
|
||||
fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
|
||||
if (!isAddToPortfolioAvailable) return
|
||||
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.ViaUserPortfolio(rawCurrencyId))
|
||||
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
|
||||
}
|
||||
|
||||
private fun openTokenDetails(result: AddToPortfolioManager.Result) {
|
||||
appRouter.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = result.wallet.walletId,
|
||||
currency = result.addedCurrency.currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun addToPortfolioManagerOrNull(): AddToPortfolioManager? = addToPortfolioManager
|
||||
|
||||
private fun prepareAddToPortfolioManager(launchMode: AddToPortfolioManager.LaunchMode) {
|
||||
addToPortfolioListenersJob?.cancel()
|
||||
val manager = addToPortfolioManagerFactory.create(
|
||||
scope = modelScope,
|
||||
settings = AddToPortfolioManager.Settings(
|
||||
shouldSkipTokenActionsScreen = false,
|
||||
launchMode = launchMode,
|
||||
),
|
||||
analyticsParams = AddToPortfolioManager.AnalyticsParams(params.analyticsParams?.source),
|
||||
)
|
||||
manager.setTokenParams(params.token)
|
||||
when (val network = networksState.value) {
|
||||
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(network.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
|
||||
else -> Unit
|
||||
}
|
||||
addToPortfolioManager = manager
|
||||
addToPortfolioListenersJob = modelScope.launch {
|
||||
launch {
|
||||
manager.onDismiss.receiveAsFlow().collect {
|
||||
addToPortfolioSheetNavigation.dismiss()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
manager.onSuccessAdded.receiveAsFlow().collect {
|
||||
addToPortfolioSheetNavigation.dismiss()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
manager.onAddedTokenClick.receiveAsFlow().collect { result ->
|
||||
addToPortfolioSheetNavigation.dismiss()
|
||||
openTokenDetails(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialLoad() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package com.tangem.features.feed.model.search.converter
|
||||
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.extensions.networkIconResId
|
||||
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
|
||||
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
|
||||
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.common.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
|
|
@ -14,10 +16,8 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import com.tangem.domain.search.model.UserAssetSearchItem
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -26,7 +26,7 @@ import java.math.BigDecimal
|
|||
internal class UserAssetSearchItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onSingleClick: (UserAssetSearchEntry) -> Unit,
|
||||
private val onSingleClick: (UserAssetEntry) -> Unit,
|
||||
private val onGroupedClick: (UserAssetSearchItem.Grouped) -> Unit,
|
||||
) : Converter<UserAssetSearchItem, UserAssetItemUM> {
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ internal class UserAssetSearchItemConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun convertSingle(entry: UserAssetSearchEntry): UserAssetItemUM.Single {
|
||||
private fun convertSingle(entry: UserAssetEntry): UserAssetItemUM.Single {
|
||||
val currency = entry.currencyStatus.currency
|
||||
val value = entry.currencyStatus.value
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ internal class UserAssetSearchItemConverter(
|
|||
}
|
||||
|
||||
private fun convertGroupedBalanceState(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
entries: List<UserAssetEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState {
|
||||
|
|
@ -145,7 +145,7 @@ internal class UserAssetSearchItemConverter(
|
|||
}
|
||||
|
||||
private fun computeGroupBalance(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
entries: List<UserAssetEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState.Loaded {
|
||||
|
|
@ -158,7 +158,7 @@ internal class UserAssetSearchItemConverter(
|
|||
}
|
||||
|
||||
private fun computeGroupBalanceFlickering(
|
||||
entries: List<UserAssetSearchEntry>,
|
||||
entries: List<UserAssetEntry>,
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
): BalanceDisplayState.Flickering {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.markets.tokenselector.AccountHeaderData
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.features.feed.ui.search.state.AccountHeaderData
|
||||
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
|
||||
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class BuildTokenSelectorSectionsTransformer(
|
||||
private val entries: List<UserAssetSearchEntry>,
|
||||
private val entries: List<UserAssetEntry>,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onTokenSelected: (UserAssetSearchEntry) -> Unit,
|
||||
private val onTokenSelected: (UserAssetEntry) -> Unit,
|
||||
) : TokenSelectorUMTransformer {
|
||||
|
||||
private val entryConverter = TokenSelectorEntryConverter(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.feed.model.search.state.transformers
|
||||
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
|
||||
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
|
|
@ -11,9 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -22,12 +22,12 @@ import java.math.BigDecimal
|
|||
internal class TokenSelectorEntryConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val onTokenSelected: (UserAssetSearchEntry) -> Unit,
|
||||
) : Converter<UserAssetSearchEntry, UserAssetItemUM.Single> {
|
||||
private val onTokenSelected: (UserAssetEntry) -> Unit,
|
||||
) : Converter<UserAssetEntry, UserAssetItemUM.Single> {
|
||||
|
||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: UserAssetSearchEntry): UserAssetItemUM.Single {
|
||||
override fun convert(value: UserAssetEntry): UserAssetItemUM.Single {
|
||||
val currency = value.currencyStatus.currency
|
||||
val currencyValue = value.currencyStatus.value
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ package com.tangem.features.feed.ui.search.state
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.tokenselector.UserAssetItemUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SearchUM(
|
||||
|
|
@ -43,59 +41,4 @@ sealed interface MarketSearchResultUM {
|
|||
data object Empty : MarketSearchResultUM
|
||||
}
|
||||
|
||||
data class TextHintItemUM(val text: String)
|
||||
|
||||
@Immutable
|
||||
sealed interface BalanceDisplayState {
|
||||
|
||||
data class Loaded(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data class Flickering(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data class Stale(
|
||||
val cryptoBalance: TextReference,
|
||||
val fiatBalance: TextReference,
|
||||
) : BalanceDisplayState
|
||||
|
||||
data object Loading : BalanceDisplayState
|
||||
data object Unreachable : BalanceDisplayState
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface UserAssetItemUM {
|
||||
val id: String
|
||||
val icon: TangemIconUM
|
||||
val tokenName: String
|
||||
val tokenSymbol: String
|
||||
val onClick: () -> Unit
|
||||
|
||||
data class Single(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val fiatRate: String?,
|
||||
val priceChangeState: PriceChangeState,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val networkName: String,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
|
||||
data class Grouped(
|
||||
override val id: String,
|
||||
override val icon: TangemIconUM,
|
||||
override val tokenName: String,
|
||||
override val tokenSymbol: String,
|
||||
val tokensCount: Int,
|
||||
val balanceState: BalanceDisplayState,
|
||||
val isBalanceHidden: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : UserAssetItemUM
|
||||
}
|
||||
data class TextHintItemUM(val text: String)
|
||||
|
|
@ -8,9 +8,9 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.features.feed.ui.search.state.TokenSelectorContentUM
|
||||
import com.tangem.features.feed.ui.search.state.TokenSelectorSectionUM
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
|
||||
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
|
||||
import io.mockk.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
|
@ -22,7 +22,7 @@ import java.math.BigDecimal
|
|||
class BuildTokenSelectorSectionsTransformerTest {
|
||||
|
||||
private val appCurrency: AppCurrency = AppCurrency.Default
|
||||
private val onTokenSelected: (UserAssetSearchEntry) -> Unit = mockk(relaxed = true)
|
||||
private val onTokenSelected: (UserAssetEntry) -> Unit = mockk(relaxed = true)
|
||||
private val prevState = TokenSelectorContentUM(sections = persistentListOf())
|
||||
|
||||
@BeforeEach
|
||||
|
|
@ -231,7 +231,7 @@ class BuildTokenSelectorSectionsTransformerTest {
|
|||
|
||||
// region Helpers
|
||||
|
||||
private fun createTransformer(entries: List<UserAssetSearchEntry>): BuildTokenSelectorSectionsTransformer {
|
||||
private fun createTransformer(entries: List<UserAssetEntry>): BuildTokenSelectorSectionsTransformer {
|
||||
return BuildTokenSelectorSectionsTransformer(
|
||||
entries = entries,
|
||||
appCurrency = appCurrency,
|
||||
|
|
@ -259,7 +259,7 @@ class BuildTokenSelectorSectionsTransformerTest {
|
|||
currencyId: String = "btc",
|
||||
currencyName: String = "Bitcoin",
|
||||
currencySymbol: String = "BTC",
|
||||
): UserAssetSearchEntry {
|
||||
): UserAssetEntry {
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { name } returns "Network"
|
||||
}
|
||||
|
|
@ -290,7 +290,7 @@ class BuildTokenSelectorSectionsTransformerTest {
|
|||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
}
|
||||
return mockk<UserAssetSearchEntry> {
|
||||
return mockk<UserAssetEntry> {
|
||||
every { userWalletId } returns walletId
|
||||
every { userWalletName } returns walletName
|
||||
every { this@mockk.accountId } returns accountId
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.search.model.UserAssetSearchEntry
|
||||
import com.tangem.features.feed.ui.search.state.BalanceDisplayState
|
||||
import com.tangem.domain.models.portfolio.UserAssetEntry
|
||||
import com.tangem.common.ui.markets.tokenselector.BalanceDisplayState
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -23,7 +23,7 @@ import java.math.BigDecimal
|
|||
class TokenSelectorEntryConverterTest {
|
||||
|
||||
private val appCurrency: AppCurrency = AppCurrency.Default
|
||||
private val onTokenSelected: (UserAssetSearchEntry) -> Unit = mockk(relaxed = true)
|
||||
private val onTokenSelected: (UserAssetEntry) -> Unit = mockk(relaxed = true)
|
||||
|
||||
private lateinit var converter: TokenSelectorEntryConverter
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ class TokenSelectorEntryConverterTest {
|
|||
fiatRate = BigDecimal("30000.0"),
|
||||
priceChange = BigDecimal("1.0"),
|
||||
),
|
||||
): UserAssetSearchEntry {
|
||||
): UserAssetEntry {
|
||||
val userWalletId = mockk<UserWalletId> {
|
||||
every { stringValue } returns walletId
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ class TokenSelectorEntryConverterTest {
|
|||
every { this@mockk.currency } returns currency
|
||||
every { this@mockk.value } returns value
|
||||
}
|
||||
return mockk<UserAssetSearchEntry> {
|
||||
return mockk<UserAssetEntry> {
|
||||
every { this@mockk.userWalletId } returns userWalletId
|
||||
every { this@mockk.userWalletName } returns "Wallet"
|
||||
every { this@mockk.accountId } returns accountIdMock
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue