Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-25 19:06:24 +04:00
parent 7fe610ec37
commit 435e579a61
21 changed files with 78 additions and 1949 deletions

View file

@ -58,7 +58,6 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
bottomSheet.child?.instance?.BottomSheet()
}
@Suppress("UnsafeCallOnNullableType")
private fun bottomSheetChild(
config: MarketsPortfolioRoute,
componentContext: ComponentContext,
@ -66,7 +65,7 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = model.newAddToPortfolioManager!!,
addToPortfolioManager = model.addToPortfolioManager,
callback = model.addToPortfolioCallback,
),
)

View file

@ -1,135 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.models.YieldSupplyAvailability
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Inject
/**
* Loader of portfolio data
*
* @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses
* @property getSelectedAppCurrencyUseCase use case for getting selected app currency
* @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings
* @property getWalletTotalBalanceUseCase use case for getting wallet total balance
*
[REDACTED_AUTHOR]
*/
internal class PortfolioDataLoader @Inject constructor(
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
) {
/** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencies = walletsWithCurrencies,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
walletsWithBalance = emptyMap(),
)
}
// setup balances for wallets from walletsWithCurrencyStatuses
.flatMapLatest { portfolioData ->
getWalletsWithTotalBalanceFlow(
ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId),
)
.map { portfolioData.copy(walletsWithBalance = it) }
.onEmpty { emit(portfolioData) }
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getAllWalletsCryptoCurrenciesData(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged()
.map { walletsWithMaybeStatuses ->
walletsWithMaybeStatuses.mapValues { entry ->
entry.value.mapNotNull { it.getOrNull() }
}
}
.flatMapLatest { walletsWithStatuses ->
val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) ->
statuses.map { status ->
val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse {
YieldSupplyAvailability.Unavailable
}
getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability)
.map { tokenActionsState ->
PortfolioData.CryptoCurrencyData(
userWallet = wallet,
status = status,
actions = tokenActionsState.states,
)
}
}
}
combine(actionsFlows) { actions ->
walletsWithStatuses.mapValues { entry ->
entry.value.mapNotNull { status ->
actions.firstOrNull {
it.userWallet == entry.key && it.status == status
}
}
}
}.onEmpty {
emit(
walletsWithStatuses.mapValues { (wallet, statuses) ->
statuses.map { status ->
PortfolioData.CryptoCurrencyData(
userWallet = wallet,
status = status,
actions = emptyList(),
)
}
},
)
}
}.onEmpty {
emit(emptyMap())
}
.distinctUntilChanged()
}
private fun getWalletsWithTotalBalanceFlow(
ids: List<UserWalletId>,
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {
return combine(
flows = ids
.map { userWalletId ->
getWalletTotalBalanceUseCase(userWalletId)
.map { userWalletId to it }
.distinctUntilChanged()
},
transform = { it.toMap() },
)
.distinctUntilChanged()
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
}
}

View file

@ -1,161 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
import kotlinx.collections.immutable.toImmutableList
/**
* Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig]
*
* @property token token params
* @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed
* @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed
* @property onNetworkSwitchClick callback is invoked when network switch is clicked
* @property onAnotherWalletSelect callback is invoked when wallet is selected
* @property onContinueClick callback is invoked when continue button is clicked
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class AddToPortfolioBSContentUMFactory(
private val addToPortfolioManager: AddToPortfolioManager,
private val token: TokenMarketParams,
private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit,
private val onWalletSelectorVisibilityChange: (Boolean) -> Unit,
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
private val onAnotherWalletSelect: (UserWalletId) -> Unit,
private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) -> Unit,
) {
/**
* Create [TangemBottomSheetConfig]
*
* @param portfolioData portfolio data
* @param portfolioUIData portfolio bottom sheet visibility model
* @param selectedWallet selected wallet
* @param alreadyAddedNetworks already added networks
*/
@Suppress("LongParameterList")
fun create(
currentState: TangemBottomSheetConfig?,
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
selectedWallet: UserWallet?,
alreadyAddedNetworks: Set<String>?,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return (currentState ?: TangemBottomSheetConfig.Empty).copy(
isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible,
onDismissRequest = { onAddToPortfolioVisibilityChange(false) },
content = if (selectedWallet != null && alreadyAddedNetworks != null) {
AddToPortfolioBSContentUM(
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(
portfolioData = portfolioData,
balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(),
artwork = artworks[selectedWallet.walletId],
),
selectNetworkUM = SelectNetworkUMConverter(
networksWithToggle = addToPortfolioManager.associateWithToggle(
userWalletId = selectedWallet.walletId,
alreadyAddedNetworkIds = alreadyAddedNetworks,
addToPortfolioData = portfolioUIData.addToPortfolioData,
),
alreadyAddedNetworks = alreadyAddedNetworks,
onNetworkSwitchClick = onNetworkSwitchClick,
).convert(value = token),
isScanCardNotificationVisible = portfolioUIData.isNeededColdWalletInteraction,
isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks(
userWalletId = selectedWallet.walletId,
),
onContinueButtonClick = {
onContinueClick(
selectedWallet.walletId,
portfolioUIData.addToPortfolioData.getAddedNetworks(
userWalletId = selectedWallet.walletId,
alreadyAddedNetworkIds = alreadyAddedNetworks,
),
)
},
walletSelectorConfig = createWalletSelectorBSConfig(
isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible,
portfolioData = portfolioData,
selectedWalletId = selectedWallet.walletId,
artworks = artworks,
),
isWalletBlockVisible = portfolioData.walletsWithCurrencies
.filterKeys(UserWallet::isMultiCurrency).size > 1,
)
} else {
TangemBottomSheetConfigContent.Empty
},
)
}
private fun UserWallet.toSelectedUserWalletItemUM(
artwork: UserWalletItemUM.ImageState? = null,
portfolioData: PortfolioData,
balance: TotalFiatBalance?,
): UserWalletItemUM {
return UserWalletItemUMConverter(
onClick = { onWalletSelectorVisibilityChange(true) },
endIcon = UserWalletItemUM.EndIcon.Arrow,
balance = balance,
artwork = artwork,
appCurrency = portfolioData.appCurrency,
isBalanceHidden = portfolioData.isBalanceHidden,
).convert(value = this)
}
private fun createWalletSelectorBSConfig(
isShow: Boolean,
portfolioData: PortfolioData,
selectedWalletId: UserWalletId,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
return TangemBottomSheetConfig(
isShown = isShow,
onDismissRequest = { onWalletSelectorVisibilityChange(false) },
content = WalletSelectorBSContentUM(
userWallets = portfolioData.walletsWithCurrencies
.filterKeys(UserWallet::isMultiCurrency)
.map { it.key }
.map { userWallet ->
val balance = portfolioData.walletsWithBalance[userWallet.walletId]
UserWalletItemUMConverter(
onClick = { id ->
if (id != selectedWalletId) {
onAnotherWalletSelect(id)
onWalletSelectorVisibilityChange(false)
}
},
appCurrency = portfolioData.appCurrency,
balance = balance?.getOrNull(),
isBalanceHidden = portfolioData.isBalanceHidden,
endIcon = if (userWallet.walletId == selectedWalletId) {
UserWalletItemUM.EndIcon.Checkmark
} else {
UserWalletItemUM.EndIcon.None
},
artwork = artworks[userWallet.walletId],
).convert(userWallet)
}
.toImmutableList(),
onBack = { onWalletSelectorVisibilityChange(false) },
),
)
}
}

View file

@ -1,193 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.firstOrNull
import kotlin.collections.orEmpty
import kotlin.collections.set
internal typealias WalletsWithNetworks = Map<UserWalletId, Set<TokenMarketInfo.Network>>
/**
* Manager for tracking changing networks in AddToPortfolio
*
[REDACTED_AUTHOR]
*/
internal class AddToPortfolioManager @Inject constructor(
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
) {
val availableNetworks = MutableStateFlow<Set<TokenMarketInfo.Network>?>(value = null)
private val addedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
private val removedNetworks = MutableStateFlow<WalletsWithNetworks>(value = emptyMap())
/** Get [AddToPortfolioData] as flow */
fun getAddToPortfolioData(): Flow<AddToPortfolioData> {
return combine(
flow = availableNetworks,
flow2 = addedNetworks,
flow3 = removedNetworks,
transform = ::AddToPortfolioData,
)
}
/** Set available networks [networks] */
fun setAvailableNetworks(networks: List<TokenMarketInfo.Network>) {
availableNetworks.value = networks.toSet()
}
/** Add network [networkId] to [userWalletId] */
fun addNetwork(userWalletId: UserWalletId, networkId: String) {
addedNetworks.add(userWalletId, networkId)
removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId)
}
/** Remove network [networkId] from [userWalletId] */
fun removeNetwork(userWalletId: UserWalletId, networkId: String) {
removedNetworks.add(userWalletId, networkId)
addedNetworks.cancelPrevChangeIfExist(
userWalletId = userWalletId,
networkId = networkId,
)
}
/** Remove all networks by [userWalletId] */
fun removeAllChanges(userWalletId: UserWalletId) {
addedNetworks.update {
it.toMutableMap().apply { remove(userWalletId) }
}
removedNetworks.update {
it.toMutableMap().apply { remove(userWalletId) }
}
}
fun associateWithToggle(
userWalletId: UserWalletId,
alreadyAddedNetworkIds: Set<String>,
addToPortfolioData: AddToPortfolioData,
): Map<TokenMarketInfo.Network, Boolean> {
val filteredNetworks = filterAvailableNetworksForWalletUseCase(
userWalletId = userWalletId,
networks = addToPortfolioData.availableNetworks.orEmpty(),
)
// Use user choice or check already added networks
return filteredNetworks.associateWith { availableNetwork ->
val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork)
if (isAddedByUser == true) return@associateWith true
val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork)
if (isRemovedByUser == true) return@associateWith false
val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId }
isAddedBefore
}
}
private fun MutableStateFlow<WalletsWithNetworks>.cancelPrevChangeIfExist(
userWalletId: UserWalletId,
networkId: String,
) {
if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId)
}
private fun MutableStateFlow<WalletsWithNetworks>.add(userWalletId: UserWalletId, networkId: String) {
change(userWalletId = userWalletId, networkId = networkId, isAddAction = true)
}
private fun MutableStateFlow<WalletsWithNetworks>.remove(userWalletId: UserWalletId, networkId: String) {
change(userWalletId = userWalletId, networkId = networkId, isAddAction = false)
}
private fun MutableStateFlow<WalletsWithNetworks>.change(
userWalletId: UserWalletId,
networkId: String,
isAddAction: Boolean,
) {
val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId }
if (network == null) {
Timber.d(
"Network [$networkId] doesn't contain in available networks [%s]",
availableNetworks.value?.joinToString { it.networkId },
)
return
}
update { walletsWithNetworks ->
walletsWithNetworks.toMutableMap().apply {
this[userWalletId] = if (isAddAction) {
this[userWalletId].orEmpty() + network
} else {
this[userWalletId].orEmpty() - network
}
}
}
}
/**
* Add to portfolio data
*
* @property availableNetworks available networks that user can add to portfolio
* @property addedNetworks networks that user toggled on, but it might have already been added to the wallet
* @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet
*
* Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just
* toggle it. But when we will save user changes, we will check what tokens have already been added or
* haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks]
*/
data class AddToPortfolioData(
val availableNetworks: Set<TokenMarketInfo.Network>?,
val addedNetworks: WalletsWithNetworks,
val removedNetworks: WalletsWithNetworks,
) {
fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean {
return addedNetworks[userWalletId].orEmpty().isNotEmpty()
}
fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean {
return addedNetworks[userWalletId].orEmpty().isNotEmpty() ||
removedNetworks[userWalletId].orEmpty().isNotEmpty()
}
/** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */
fun getAddedNetworks(
userWalletId: UserWalletId,
alreadyAddedNetworkIds: Set<String>,
): Set<TokenMarketInfo.Network> {
val addedNetworksByUser = addedNetworks[userWalletId].orEmpty()
return addedNetworksByUser.map { it.networkId }
.minus(alreadyAddedNetworkIds)
.mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } }
.toSet()
}
/** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */
fun getRemovedNetworks(
userWalletId: UserWalletId,
alreadyAddedNetworkIds: Set<String>,
): Set<TokenMarketInfo.Network> {
val removedNetworksByUser = removedNetworks[userWalletId].orEmpty()
return alreadyAddedNetworkIds
.minus(removedNetworksByUser.map { it.networkId }.toSet())
.mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } }
.toSet()
}
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.models.AccountStatusList
@ -45,7 +44,7 @@ import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongParameterList")
internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
internal class MarketsPortfolioDelegate @AssistedInject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val allAccountSupplier: MultiAccountStatusListSupplier,
@ -106,10 +105,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
private fun addFirstTokenFlow(): Flow<MyPortfolioUM> = buttonState.map { state ->
when (state) {
MyPortfolioUM.Tokens.AddButtonState.Loading -> MyPortfolioUM.Loading
MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(
onAddClick = onAddClick,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
)
MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(onAddClick = onAddClick)
MyPortfolioUM.Tokens.AddButtonState.Unavailable -> MyPortfolioUM.Unavailable
}
}
@ -314,7 +310,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
tokenActionsHandler: TokenActionsHandler,
buttonState: Flow<MyPortfolioUM.Tokens.AddButtonState>,
onAddClick: () -> Unit,
): NewMarketsPortfolioDelegate
): MarketsPortfolioDelegate
}
}

View file

@ -5,91 +5,48 @@ 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.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.markets.SaveMarketTokensUseCase
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.feed.impl.R
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager as NewAddToPortfolioManager
@Suppress("LongParameterList", "LargeClass")
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
tokenActionsIntentsFactory: TokenActionsHandler.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val messageSender: UiMessageSender,
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val portfolioDataLoader: PortfolioDataLoader,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val userWalletImageFetcher: UserWalletImageFetcher,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val marketsPortfolioDelegateFactory: MarketsPortfolioDelegate.Factory,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val tokenActionsHandlerFactory: TokenActionsHandler.Factory,
private val receiveAddressesFactory: ReceiveAddressesFactory,
accountsFeatureToggles: AccountsFeatureToggles,
newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory,
newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
val state: StateFlow<MyPortfolioUM> get() = _state
val state: StateFlow<MyPortfolioUM>
field = MutableStateFlow<MyPortfolioUM>(value = MyPortfolioUM.Loading)
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.token.symbol,
source = params.analyticsParams?.source,
)
val newAddToPortfolioManager: NewAddToPortfolioManager?
val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate?
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
val addToPortfolioManager: AddToPortfolioManager = createAddToPortfolioManager()
private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate()
val bottomSheetNavigation: SlotNavigation<MarketsPortfolioRoute> = SlotNavigation()
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
@ -97,321 +54,87 @@ internal class MarketsPortfolioModel @Inject constructor(
override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss()
}
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
onHandleQuickAction = { handledAction ->
analyticsEventHandler.send(
analyticsEventBuilder.quickActionClick(
actionUM = handledAction.action,
blockchainName = handledAction
.cryptoCurrencyData
.status
.currency
.network
.name,
),
)
configureReceiveAddresses(handledAction)
},
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.token.symbol,
source = params.analyticsParams?.source,
)
private val factory = MyPortfolioUMFactory(
onAddClick = {
onAddToPortfolioBSVisibilityChange(isShow = true)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioClicked(),
)
},
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
addToPortfolioManager = addToPortfolioManager,
token = params.token,
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
onNetworkSwitchClick = ::onNetworkSwitchClick,
onAnotherWalletSelect = { walletId ->
onWalletSelect(walletId)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioWalletChanged(),
)
},
onContinueClick = { selectedWalletId, addedNetworks ->
onContinueClick(selectedWalletId, addedNetworks)
private val currentAppCurrency = createAppCurrencyFlow()
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioContinue(
blockchainNames = addedNetworks.mapNotNull {
BlockchainUtils.getNetworkInfo(it.networkId)?.name
},
),
)
},
),
currentState = Provider { _state.value },
tokenActionsHandler = tokenActionsHandler,
updateTokens = { updateBlock ->
updateTokensState { state ->
state.copy(tokens = updateBlock(state.tokens))
}
},
)
private val tokenActionsHandler = createTokenActionsHandler()
init {
if (accountsFeatureToggles.isFeatureEnabled) {
newAddToPortfolioManager = newAddToPortfolioManagerFactory
.create(
modelScope,
params.token,
params.analyticsParams?.source?.let { NewAddToPortfolioManager.AnalyticsParams(it) },
)
newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create(
scope = modelScope,
token = params.token,
tokenActionsHandler = tokenActionsHandler,
buttonState = newAddToPortfolioManager.state.map { state ->
when (state) {
is NewAddToPortfolioManager.State.AvailableToAdd -> {
MyPortfolioUM.Tokens.AddButtonState.Available
}
NewAddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
NewAddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
}
},
onAddClick = {
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
},
)
newMarketsPortfolioDelegate.combineData()
.onEach { _state.value = it }
.flowOn(dispatchers.default)
.launchIn(modelScope)
} else {
newAddToPortfolioManager = null
newMarketsPortfolioDelegate = null
// Subscribe on selected wallet flow to support actual selected wallet
subscribeOnSelectedMultiWalletUpdates()
subscribeOnStateUpdates()
}
marketsPortfolioDelegate.combineData()
.onEach { state.value = it }
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
addToPortfolioManager.setAvailableNetworks(networks)
newAddToPortfolioManager?.setTokenNetworks(networks)
newMarketsPortfolioDelegate?.setTokenNetworks(networks)
addToPortfolioManager.setTokenNetworks(networks)
marketsPortfolioDelegate.setTokenNetworks(networks)
}
fun setNoNetworksAvailable() {
addToPortfolioManager.setAvailableNetworks(emptyList())
newAddToPortfolioManager?.setTokenNetworks(emptyList())
newMarketsPortfolioDelegate?.setTokenNetworks(emptyList())
addToPortfolioManager.setTokenNetworks(emptyList())
marketsPortfolioDelegate.setTokenNetworks(emptyList())
}
private fun subscribeOnSelectedMultiWalletUpdates() {
getSelectedWalletUseCase()
.getOrElse { e ->
Timber.e("Failed to load selected wallet: $e")
error("Failed to load selected wallet")
}
.onEach { userWallet ->
selectedMultiWalletIdFlow.value = userWallet.takeIf { it.isMultiCurrency }?.walletId
}
.launchIn(modelScope)
}
private fun subscribeOnStateUpdates() {
combine(
flow = loadPortfolioDataWithArtworks(params.token.id),
flow2 = getPortfolioUIDataFlow(),
transform = { pair, portfolioUIData ->
val (portfolioData, artworks) = pair
factory.create(portfolioData, portfolioUIData, artworks)
},
)
.onEach { _state.value = it }
.launchIn(modelScope)
}
private fun loadPortfolioDataWithArtworks(
currencyRawId: CryptoCurrency.RawID,
): Flow<Pair<PortfolioData, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
val wallets = Channel<Set<UserWallet>>()
val portfolioFlow = portfolioDataLoader
.load(currencyRawId)
.onEach { wallets.trySend(it.walletsWithCurrencies.keys) }
val artworksFlow = wallets.receiveAsFlow()
.distinctUntilChanged()
.flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
return combine(
flow = portfolioFlow,
flow2 = artworksFlow,
) { portfolioData, artworks -> portfolioData to artworks }
}
private fun getPortfolioUIDataFlow(): Flow<PortfolioUIData> {
return combine(
flow = portfolioBSVisibilityModelFlow,
flow2 = selectedMultiWalletIdFlow,
flow3 = addToPortfolioManager.getAddToPortfolioData(),
transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData ->
PortfolioUIData(
portfolioBSVisibilityModel = portfolioBSVisibilityModel,
selectedWalletId = selectedWalletId,
addToPortfolioData = addToPortfolioData,
isNeededColdWalletInteraction = isNeededColdWalletInteraction(selectedWalletId, addToPortfolioData),
)
},
private fun createAddToPortfolioManager(): AddToPortfolioManager {
return addToPortfolioManagerFactory.create(
scope = modelScope,
token = params.token,
analyticsParams = params.analyticsParams?.source?.let { AddToPortfolioManager.AnalyticsParams(it) },
)
}
private suspend fun isNeededColdWalletInteraction(
selectedWalletId: UserWalletId?,
addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
): Boolean {
return if (selectedWalletId != null) {
coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = selectedWalletId,
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
.associate { it.networkId to null },
)
} else {
false
}
}
private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) {
val selectedWalletId = selectedMultiWalletIdFlow.value
if (selectedWalletId == null) {
Timber.e("Impossible to switch network when selected wallet is null")
return
}
if (isChecked) {
modelScope.launch {
val unsupportedState = checkCurrencyUnsupportedState(
userWalletId = selectedWalletId,
rawNetworkId = blockchainRowUM.id,
isMainNetwork = blockchainRowUM.isMainNetwork,
)
if (unsupportedState != null) {
showUnsupportedWarning(unsupportedState)
} else {
addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
private fun createMarketsPortfolioDelegate(): MarketsPortfolioDelegate {
return marketsPortfolioDelegateFactory.create(
scope = modelScope,
token = params.token,
tokenActionsHandler = tokenActionsHandler,
buttonState = addToPortfolioManager.state.map { state ->
when (state) {
is AddToPortfolioManager.State.AvailableToAdd -> {
MyPortfolioUM.Tokens.AddButtonState.Available
}
AddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
AddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
}
}
} else {
addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id)
}
}
private suspend fun checkCurrencyUnsupportedState(
userWalletId: UserWalletId,
rawNetworkId: String,
isMainNetwork: Boolean,
): CurrencyUnsupportedState? {
return checkCurrencyUnsupportedUseCase(
userWalletId = userWalletId,
networkId = rawNetworkId,
isMainNetwork = isMainNetwork,
).getOrElse { throwable ->
Timber.e(
throwable,
"""
Failed to check currency unsupported state
|- User wallet ID: $userWalletId
|- Network ID: $rawNetworkId
|- Is main network: $isMainNetwork
""".trimIndent(),
)
val message = SnackbarMessage(
message = throwable.localizedMessage
?.let(::stringReference)
?: resourceReference(R.string.common_error),
)
messageSender.send(message)
null
}
}
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
val message = DialogMessage(
message = when (unsupportedState) {
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
id = R.string.alert_manage_tokens_unsupported_curve_message,
formatArgs = wrappedList(unsupportedState.networkName),
)
},
onAddClick = {
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
},
)
messageSender.send(message)
}
private fun onWalletSelect(userWalletId: UserWalletId) {
selectedMultiWalletIdFlow.update { prevUserWalletId ->
prevUserWalletId?.let(addToPortfolioManager::removeAllChanges)
userWalletId
}
}
private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set<TokenMarketInfo.Network>) {
modelScope.launch {
saveMarketTokensUseCase(
userWalletId = userWalletId,
tokenMarketParams = params.token,
addedNetworks = addedNetworks,
removedNetworks = emptySet(),
private fun createAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
onAddToPortfolioBSVisibilityChange(isShow = false)
addToPortfolioManager.removeAllChanges(userWalletId)
}
}
private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) {
portfolioBSVisibilityModelFlow.update {
it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false)
}
}
private fun onWalletSelectorVisibilityChange(isShow: Boolean) {
portfolioBSVisibilityModelFlow.update {
it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow)
}
}
private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) {
_state.update { stateToUpdate ->
val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate
block(tokensState)
}
private fun createTokenActionsHandler(): TokenActionsHandler {
return tokenActionsHandlerFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
onHandleQuickAction = { handledAction ->
val currency = handledAction.cryptoCurrencyData.status.currency
analyticsEventHandler.send(
analyticsEventBuilder.quickActionClick(
actionUM = handledAction.action,
blockchainName = currency.network.name,
),
)
configureReceiveAddresses(handledAction)
},
)
}
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {

View file

@ -1,153 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
/**
* Factory for creating [MyPortfolioUM]
*
* @property onAddClick callback when user wants to add new token
* @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content
* @property tokenActionsHandler token actions handler
* @property currentState current state provider
* @property updateTokens callback for updating tokens
*
[REDACTED_AUTHOR]
*/
internal class MyPortfolioUMFactory(
private val onAddClick: () -> Unit,
private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory,
private val tokenActionsHandler: TokenActionsHandler,
private val currentState: Provider<MyPortfolioUM>,
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
) {
fun create(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): MyPortfolioUM {
val addToPortfolioData = portfolioUIData.addToPortfolioData
val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable
val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) {
portfolioData.walletsWithCurrencies
} else {
portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks)
}
val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty()
if (isPortfolioEmpty) {
val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty()
return if (hasMultiWallets) {
MyPortfolioUM.AddFirstToken(
addToPortfolioBSConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
artworks = artworks,
),
onAddClick = onAddClick,
)
} else {
MyPortfolioUM.UnavailableForWallet
}
}
return TokensPortfolioUMConverter(
appCurrency = portfolioData.appCurrency,
isBalanceHidden = portfolioData.isBalanceHidden,
addButtonState = walletsWithCurrencies.getAddButtonState(
availableNetworks = addToPortfolioData.availableNetworks,
),
bsConfig = createAddToPortfolioBSConfig(
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
artworks = artworks,
),
onAddClick = onAddClick,
quickActionsIntents = tokenActionsHandler,
currentState = currentState,
updateTokens = updateTokens,
)
.convert(walletsWithCurrencies)
}
private fun createAddToPortfolioBSConfig(
portfolioData: PortfolioData,
portfolioUIData: PortfolioUIData,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): TangemBottomSheetConfig {
val selectedWallet = portfolioData.walletsWithCurrencies.keys
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }
?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }
val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty()
val alreadyAddedNetworks = portfolioData.walletsWithCurrencies
.filterAvailableNetworks(availableNetworks)[selectedWallet]
?.filter { !it.status.currency.isCustom }
?.map { it.status.currency.network.backendId }
?.toSet()
return addToPortfolioBSContentUMFactory.create(
currentState = currentState().addToPortfolioBSConfig,
portfolioData = portfolioData,
portfolioUIData = portfolioUIData,
selectedWallet = selectedWallet,
alreadyAddedNetworks = alreadyAddedNetworks,
artworks = artworks,
)
}
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.getAddButtonState(
availableNetworks: Set<TokenMarketInfo.Network>?,
): MyPortfolioUM.Tokens.AddButtonState {
if (availableNetworks == null) return MyPortfolioUM.Tokens.AddButtonState.Loading
val networkIds = availableNetworks.map { it.networkId }
val isAllAvailableNetworksAdded = this
// User can add currencies only in multi-currency wallets
.filterKeys(UserWallet::isMultiCurrency)
.mapValues { entry -> entry.value.map { it.status.currency.network.backendId } }
// Each wallets contains all available networks?
.all { it.value.containsAll(networkIds) }
return if (isAllAvailableNetworksAdded) {
MyPortfolioUM.Tokens.AddButtonState.Unavailable
} else {
MyPortfolioUM.Tokens.AddButtonState.Available
}
}
/** Filter map values by available networks [networks] */
private fun Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>.filterAvailableNetworks(
networks: Set<TokenMarketInfo.Network>,
): Map<UserWallet, List<PortfolioData.CryptoCurrencyData>> {
return mapValues { entry -> entry.value.filterAvailableNetworks(networks) }
}
/** Filter list of [CryptoCurrencyStatus] by available networks [networks] */
private fun List<PortfolioData.CryptoCurrencyData>.filterAvailableNetworks(
networks: Set<TokenMarketInfo.Network>,
): List<PortfolioData.CryptoCurrencyData> {
val networkIds = networks.map(TokenMarketInfo.Network::networkId)
return mapNotNull { cryptoCurrencyData ->
cryptoCurrencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) }
}
}
}

View file

@ -1,14 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
/**
* Model for portfolio bottom sheet visibility
*
* @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet
* @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet
*
[REDACTED_AUTHOR]
*/
internal data class PortfolioBSVisibilityModel(
val isAddToPortfolioBSVisible: Boolean = false,
val isWalletSelectorBSVisible: Boolean = false,
)

View file

@ -1,20 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.domain.models.wallet.UserWalletId
/**
* Portfolio UI data. Combined data from all UI flows that required to setup portfolio
*
* @property portfolioBSVisibilityModel portfolio bottom sheet visibility model
* @property selectedWalletId selected wallet id
* @property addToPortfolioData add to portfolio data
* @property isNeededColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet
*
[REDACTED_AUTHOR]
*/
internal data class PortfolioUIData(
val portfolioBSVisibilityModel: PortfolioBSVisibilityModel,
val selectedWalletId: UserWalletId?,
val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData,
val isNeededColdWalletInteraction: Boolean,
)

View file

@ -1,37 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
/**
* Converter from [TokenMarketParams] to [SelectNetworkUM]
*
* @property networksWithToggle map of networks with toggles
* @property alreadyAddedNetworks already added networks
* @property onNetworkSwitchClick callback is called when network switch is clicked
*
[REDACTED_AUTHOR]
*/
internal class SelectNetworkUMConverter(
private val networksWithToggle: Map<TokenMarketInfo.Network, Boolean>,
private val alreadyAddedNetworks: Set<String>,
private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
) : Converter<TokenMarketParams, SelectNetworkUM> {
override fun convert(value: TokenMarketParams): SelectNetworkUM {
return SelectNetworkUM(
tokenId = value.id.value,
iconUrl = value.imageUrl,
tokenName = value.name,
tokenCurrencySymbol = value.symbol,
networks = BlockchainRowUMConverter(alreadyAddedNetworks)
.convertList(networksWithToggle.toList())
.toImmutableList(),
onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) },
)
}
}

View file

@ -1,112 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens]
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class TokensPortfolioUMConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val addButtonState: MyPortfolioUM.Tokens.AddButtonState,
private val bsConfig: TangemBottomSheetConfig,
private val onAddClick: () -> Unit,
private val quickActionsIntents: TokenActionsHandler,
private val currentState: Provider<MyPortfolioUM>,
private val updateTokens: ((ImmutableList<PortfolioTokenUM>) -> ImmutableList<PortfolioTokenUM>) -> Unit,
) : Converter<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>, MyPortfolioUM.Tokens> {
override fun convert(value: Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>): MyPortfolioUM.Tokens {
val currentTokensState = currentState() as? MyPortfolioUM.Tokens
return MyPortfolioUM.Tokens(
tokens = value
.flatMap { entry -> entry.value }
.map { cryptoData ->
PortfolioTokenUMConverter(
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
onTokenItemClick = { toggleQuickActions(cryptoData) },
tokenActionsHandler = quickActionsIntents,
).convert(value = cryptoData) to cryptoData
}
.setQuickActionsVisibility(currentState = currentTokensState)
.toImmutableList(),
buttonState = addButtonState,
addToPortfolioBSConfig = bsConfig,
onAddClick = onAddClick,
)
}
private fun List<Pair<PortfolioTokenUM, PortfolioData.CryptoCurrencyData>>.setQuickActionsVisibility(
currentState: MyPortfolioUM.Tokens?,
): List<PortfolioTokenUM> {
return when {
// if there is only one token and it has empty balance, show quick actions for it
currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> {
this.map { (token, _) ->
token.copy(isQuickActionsShown = true)
}
}
// if there is no previous state, hide quick actions for all tokens
currentState == null -> {
this.map { (token, _) ->
token.copy(isQuickActionsShown = false)
}
}
else -> {
val previousList = currentState.tokens
// otherwise, keep previous state
this.map { (token, _) ->
token.copy(
isQuickActionsShown = previousList
.firstOrNull { it.matchWith(token) }
?.isQuickActionsShown == true,
)
}
}
}
}
private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
return cryptoData.status.value.amount?.isZero() == true
}
private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) {
updateTokens { tokenList ->
tokenList.map { portfolioTokenUM ->
portfolioTokenUM.copy(
isQuickActionsShown = if (portfolioTokenUM.matchWith(cryptoData)) {
!portfolioTokenUM.isQuickActionsShown
} else {
false
},
)
}.toImmutableList()
}
}
private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean {
return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id
}
private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean {
return this.walletId == cryptoData.userWallet.walletId &&
this.tokenItemState.id == cryptoData.status.currency.id.value
}
}

View file

@ -1,383 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
import com.tangem.features.feed.impl.R
import kotlinx.coroutines.delay
@Composable
internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<AddToPortfolioBSContentUM>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
addBottomInsets = false,
titleText = resourceReference(R.string.common_add_to_portfolio),
) { contentUM ->
Content(
modifier = Modifier.fillMaxWidth(),
state = contentUM,
)
WalletSelectorBottomSheet(contentUM.walletSelectorConfig)
}
}
@Composable
private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) {
var continueButtonAreaHeight by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
val scrollState = rememberScrollState()
Box(modifier = modifier) {
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
if (state.isWalletBlockVisible) {
UserWalletItem(
state = state.selectedWallet,
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
)
SpacerH12()
}
NetworkSelection(
modifier = Modifier.fillMaxWidth(),
state = state.selectNetworkUM,
)
SpacerH12()
AnimatedVisibility(
visible = state.isScanCardNotificationVisible,
modifier = Modifier.fillMaxWidth(),
) {
Column {
ScanWalletWarning(modifier = Modifier.fillMaxWidth())
SpacerH12()
}
// Scroll to the bottom when the notification appears and the scroll is at the bottom
LaunchedEffect(Unit) {
if (scrollState.canScrollForward.not()) {
delay(timeMillis = 500)
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
SpacerH(with(density) { continueButtonAreaHeight.toDp() })
}
AnimatedVisibility(
visible = scrollState.canScrollForward,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter),
) {
BottomFade(Modifier.align(Alignment.BottomCenter))
}
ContinueButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.onGloballyPositioned {
continueButtonAreaHeight = it.size.height
},
enabled = state.isContinueButtonEnabled,
isTangemIconVisible = state.isScanCardNotificationVisible,
onClick = state.onContinueButtonClick,
)
}
}
@Composable
private fun ContinueButton(
enabled: Boolean,
isTangemIconVisible: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TangemButton(
enabled = enabled,
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.navigationBarsPadding()
.fillMaxWidth(),
text = stringResourceSafe(R.string.common_continue),
icon = if (enabled && isTangemIconVisible) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
},
showProgress = false,
size = TangemButtonSize.Default,
colors = TangemButtonsDefaults.primaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
onClick = onClick,
animateContentChange = true,
)
}
@Suppress("LongMethod")
@Composable
private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) {
val hapticManager = LocalHapticManager.current
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResourceSafe(R.string.markets_select_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing14),
verticalAlignment = Alignment.CenterVertically,
) {
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size36),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
SpacerW12()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f, fill = false)
.alignByBaseline(),
text = state.tokenName,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
SpacerW6()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.alignByBaseline(),
text = state.tokenCurrencySymbol,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Visible,
maxLines = 1,
)
}
state.networks.fastForEachIndexed { index, network ->
ArrowRow(
isLastItem = index == state.networks.lastIndex,
content = {
BlockchainRow(
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
model = network,
action = {
TangemSwitch(
checked = network.isSelected,
checkedColor = if (network.isEnabled) {
TangemTheme.colors.control.checked
} else {
TangemTheme.colors.icon.inactive
},
onCheckedChange = { checked ->
if (checked) {
hapticManager.perform(TangemHapticEffect.View.ToggleOn)
} else {
hapticManager.perform(TangemHapticEffect.View.ToggleOff)
}
state.onNetworkSwitchClick(network, checked)
},
enabled = network.isEnabled,
)
},
)
},
)
}
}
}
}
@Composable
private fun ScanWalletWarning(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(
color = TangemTheme.colors.button.disabled,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
) {
Icon(
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.markets_generate_addresses_notification),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
AddToPortfolioBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
content = content,
onDismissRequest = {},
),
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContent(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContentRtl(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview(rtl = true) {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}
// For on device testing
@Composable
@Preview
private fun PreviewContentTestOnDevice(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview(
alwaysShowBottomSheets = false,
) {
var isShow by remember { mutableStateOf(false) }
var contentState by remember {
mutableStateOf(content)
}
LaunchedEffect(Unit) {
contentState = content.copy(
onContinueButtonClick = {
contentState = contentState.copy(
isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible,
)
},
isContinueButtonEnabled = true,
selectedWallet = content.selectedWallet.copy(
onClick = {
contentState = contentState.copy(
isContinueButtonEnabled = !contentState.isContinueButtonEnabled,
)
},
),
)
}
AddToPortfolioBottomSheet(
config = TangemBottomSheetConfig(
isShown = isShow,
content = contentState,
onDismissRequest = { isShow = false },
),
)
Button(
onClick = { isShow = !isShow },
) {
Text(text = "Toggle")
}
}
}

View file

@ -70,11 +70,6 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
is MyPortfolioUM.Content -> PortfolioList(state = state)
}
}
val bsConfig = state.addToPortfolioBSConfig
if (bsConfig != null) {
AddToPortfolioBottomSheet(config = bsConfig)
}
}
@Composable

View file

@ -1,86 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SimpleSettingsRow
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
import kotlinx.collections.immutable.toImmutableList
@Composable
fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<TokenActionsBSContentUM>(
config = config,
title = { content ->
TangemBottomSheetTitle(content.title)
},
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: TokenActionsBSContentUM) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.actions.forEachIndexed { index, action ->
Box(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = content.actions.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action),
) {
SimpleSettingsRow(
title = action.text.resolveReference(),
icon = action.iconRes,
redesign = true,
onItemsClick = { content.onActionClick(action) },
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
TokenActionsBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TokenActionsBSContentUM(
title = "Wallet 1",
actions = TokenActionsBSContentUM.Action.entries.toImmutableList(),
onActionClick = {},
),
),
)
}
}
}

View file

@ -1,145 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM
import com.tangem.features.feed.impl.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<WalletSelectorBSContentUM>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
addBottomInsets = false,
title = { content ->
TangemTopAppBar(
title = resourceReference(R.string.common_choose_wallet),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(content.onBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
},
) { content ->
Content(
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing8,
),
state = content,
)
}
}
@Composable
private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
BlockCard(
modifier = Modifier.fillMaxSize(),
colors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
) {
state.userWallets.forEach { state ->
key(state.id) {
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.background.action,
disabledContainerColor = TangemTheme.colors.background.action,
),
state = state,
)
}
}
}
SpacerH(bottomBarHeight)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
WalletSelectorBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = WalletSelectorBSContentUM(
userWallets = persistentListOf(
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.None,
),
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.Checkmark,
),
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.None,
),
),
onBack = {},
),
),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewContent() {
TangemThemePreview {
Content(
state = WalletSelectorBSContentUM(
userWallets = persistentListOf(
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.None,
),
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.Checkmark,
),
PreviewAddToPortfolioBSContentProvider().userWallet.copy(
endIcon = UserWalletItemUM.EndIcon.None,
),
),
onBack = {},
),
)
}
}

View file

@ -1,86 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM
import com.tangem.features.feed.impl.R
import kotlinx.collections.immutable.persistentListOf
internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider<AddToPortfolioBSContentUM> {
private val blockchainRow = BlockchainRowUM(
id = "1",
name = "Etherium 3",
type = "TEST",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = false,
)
val userWallet = UserWalletItemUM(
id = "1",
name = stringReference("Wallet 1"),
information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")),
balance = UserWalletItemUM.Balance.Loading,
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},
)
override val values: Sequence<AddToPortfolioBSContentUM>
get() = sequenceOf(
AddToPortfolioBSContentUM(
selectedWallet = userWallet,
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
blockchainRow.copy(
type = "MAIN",
isMainNetwork = true,
isSelected = true,
),
blockchainRow,
blockchainRow,
),
onNetworkSwitchClick = { _, _ -> },
iconUrl = null,
),
isScanCardNotificationVisible = true,
isWalletBlockVisible = true,
isContinueButtonEnabled = true,
onContinueButtonClick = {},
walletSelectorConfig = TangemBottomSheetConfig.Empty,
),
AddToPortfolioBSContentUM(
selectedWallet = userWallet,
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium Etherium Etherium Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
blockchainRow.copy(
type = "MAIN",
isMainNetwork = true,
isSelected = true,
).copy(name = "Etherium Etherium Etherium Etherium"),
*Array(25) { blockchainRow },
),
onNetworkSwitchClick = { _, _ -> },
iconUrl = null,
),
isScanCardNotificationVisible = true,
isWalletBlockVisible = false,
isContinueButtonEnabled = false,
onContinueButtonClick = {},
walletSelectorConfig = TangemBottomSheetConfig.Empty,
),
)
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.pre
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
@ -22,25 +21,19 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(onAddClick = {}),
MyPortfolioUM.Content(
items = persistentListOf(
walletPortfolioHeader,

View file

@ -1,15 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
internal data class AddToPortfolioBSContentUM(
val selectedWallet: UserWalletItemUM,
val selectNetworkUM: SelectNetworkUM,
val isWalletBlockVisible: Boolean,
val isScanCardNotificationVisible: Boolean,
val isContinueButtonEnabled: Boolean,
val onContinueButtonClick: () -> Unit,
val walletSelectorConfig: TangemBottomSheetConfig,
) : TangemBottomSheetConfigContent

View file

@ -1,16 +1,12 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class MyPortfolioUM {
abstract val addToPortfolioBSConfig: TangemBottomSheetConfig?
data class Tokens(
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val onAddClick: () -> Unit,
@ -27,25 +23,15 @@ internal sealed class MyPortfolioUM {
val items: ImmutableList<PortfolioListItem>,
val buttonState: Tokens.AddButtonState,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {
override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty
}
) : MyPortfolioUM()
data class AddFirstToken(
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
val onAddClick: () -> Unit,
) : MyPortfolioUM()
data object Loading : MyPortfolioUM() {
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
}
data object Loading : MyPortfolioUM()
data object Unavailable : MyPortfolioUM() {
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
}
data object Unavailable : MyPortfolioUM()
data object UnavailableForWallet : MyPortfolioUM() {
override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null
}
data object UnavailableForWallet : MyPortfolioUM()
}

View file

@ -1,13 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import kotlinx.collections.immutable.ImmutableList
internal data class SelectNetworkUM(
val tokenId: String,
val iconUrl: String?,
val tokenName: String,
val tokenCurrencySymbol: String,
val networks: ImmutableList<BlockchainRowUM>,
val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
)

View file

@ -1,10 +0,0 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
internal data class WalletSelectorBSContentUM(
val userWallets: ImmutableList<UserWalletItemUM>,
val onBack: () -> Unit,
) : TangemBottomSheetConfigContent