Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-30 14:16:57 +01:00
parent cae7844408
commit 5037fabe32
14 changed files with 602 additions and 122 deletions

View file

@ -82,5 +82,9 @@
{
"name": "SWAP_MARKET_LIST_ENABLED",
"version": "undefined"
},
{
"name": "EARN_BLOCK_ENABLED",
"version": "undefined"
}
]

View file

@ -0,0 +1,33 @@
package com.tangem.features.feed.components.market.details.portfolio.add
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent {
/**
* @param tokenToAdd token and preselected network (no network selector).
* @param callback callbacks for add-to-portfolio flow.
*/
data class Params(
val tokenToAdd: TokenToAdd,
val callback: Callback,
)
interface Callback {
fun onDismiss()
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)
}
data class TokenToAdd(
val network: TokenMarketInfo.Network,
val id: CryptoCurrency.RawID,
val name: String,
val symbol: String,
)
interface Factory : ComponentFactory<Params, AddToPortfolioPreselectedDataComponent>
}

View file

@ -2,4 +2,5 @@ package com.tangem.features.feed.entry.featuretoggle
interface FeedFeatureToggle {
val isFeedEnabled: Boolean
val isEarnBlockEnabled: Boolean
}

View file

@ -0,0 +1,118 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
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.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import com.tangem.features.feed.impl.R
@Composable
internal fun AddToPortfolioBottomSheet(
childStack: State<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
onBack: () -> Unit,
onDismiss: () -> Unit,
) {
val stack by childStack
val contentStack = remember { mutableStateOf(stack) }
val currentRoute = stack.active.configuration
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
if (isNotEmpty) {
contentStack.value = stack
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
scrollableContent = false,
onBack = onBack,
config = TangemBottomSheetConfig(
isShown = isNotEmpty,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
title = {
AnimatedContent(targetState = contentStack.value, label = "Title Animation") { animatedStack ->
AddToPortfolioBottomSheetTitle(
stack = animatedStack,
onBackClick = onBack,
modifier = Modifier.fillMaxWidth(),
)
}
},
content = {
AnimatedContent(targetState = contentStack.value, label = "Content Animation") { animatedStack ->
val paddingModifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
)
val isScrollableContent = when (animatedStack.active.configuration) {
AddToPortfolioRoutes.PortfolioSelector -> false
AddToPortfolioRoutes.AddToken,
AddToPortfolioRoutes.Empty,
is AddToPortfolioRoutes.NetworkSelector,
AddToPortfolioRoutes.TokenActions,
-> true
}
if (isScrollableContent) {
Column(
modifier = paddingModifier.verticalScroll(rememberScrollState()),
) {
animatedStack.active.instance.Content(modifier = Modifier)
}
} else {
animatedStack.active.instance.Content(modifier = paddingModifier)
}
}
},
)
}
@Composable
private fun AddToPortfolioBottomSheetTitle(
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val title: TextReference = when (stack.active.configuration) {
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
.title.collectAsStateWithLifecycle().value
}
val startIconRes: Int?
val endIconRes: Int?
if (stack.backStack.isNotEmpty()) {
startIconRes = R.drawable.ic_back_24
endIconRes = null
} else {
startIconRes = null
endIconRes = R.drawable.ic_close_24
}
TangemModalBottomSheetTitle(
modifier = modifier,
title = title,
startIconRes = startIconRes,
endIconRes = endIconRes,
onStartClick = onBackClick,
onEndClick = onBackClick,
)
}

View file

@ -1,21 +1,8 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.backStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
@ -23,19 +10,11 @@ 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.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
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.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import com.tangem.features.feed.impl.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -97,91 +76,10 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
@Composable
override fun BottomSheet() {
val stack by childStack.subscribeAsState()
val contentStack = remember { mutableStateOf(stack) }
val currentRoute = stack.active.configuration
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
if (isNotEmpty) {
contentStack.value = stack
}
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
scrollableContent = false,
AddToPortfolioBottomSheet(
childStack = childStack.subscribeAsState(),
onBack = ::onBack,
config = TangemBottomSheetConfig(
isShown = isNotEmpty,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
title = { state ->
AnimatedContent(targetState = contentStack.value) { stack ->
BottomSheetTitle(
stack = stack,
onBackClick = ::onBack,
modifier = Modifier.fillMaxWidth(),
)
}
},
content = { state ->
AnimatedContent(targetState = contentStack.value) { stack ->
val paddingModifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
)
val isScrollableContent = when (stack.active.configuration) {
AddToPortfolioRoutes.PortfolioSelector -> false
AddToPortfolioRoutes.AddToken,
AddToPortfolioRoutes.Empty,
is AddToPortfolioRoutes.NetworkSelector,
AddToPortfolioRoutes.TokenActions,
-> true
}
if (isScrollableContent) {
Column(
modifier = paddingModifier.verticalScroll(rememberScrollState()),
) {
stack.active.instance.Content(modifier = Modifier)
}
} else {
stack.active.instance.Content(modifier = paddingModifier)
}
}
},
)
}
@Composable
private fun BottomSheetTitle(
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
onBackClick: (() -> Unit),
modifier: Modifier = Modifier,
) {
val title: TextReference = when (stack.active.configuration) {
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
.title.collectAsStateWithLifecycle().value
}
val startIconRes: Int?
val endIconRes: Int?
if (stack.backStack.isNotEmpty()) {
startIconRes = R.drawable.ic_back_24
endIconRes = null
} else {
startIconRes = null
endIconRes = R.drawable.ic_close_24
}
TangemModalBottomSheetTitle(
modifier = modifier,
title = title,
startIconRes = startIconRes,
endIconRes = endIconRes,
onStartClick = onBackClick,
onEndClick = onBackClick,
onDismiss = ::dismiss,
)
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.backStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioPreselectedDataModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: AddToPortfolioPreselectedDataComponent.Params,
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
addTokenComponentFactory: AddTokenComponent.Factory,
) : AppComponentContext by context, AddToPortfolioPreselectedDataComponent {
private val model: AddToPortfolioPreselectedDataModel = getOrCreateModel(params)
private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher,
controller = model.portfolioSelectorController,
),
)
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = AddTokenComponent.Params(
eventBuilder = model.eventBuilder,
callbacks = model,
selectedPortfolio = model.selectedPortfolio,
selectedNetwork = model.selectedNetwork,
),
)
private val childStack = childStack(
key = "addToPortfolioFromEarnStack",
handleBackButton = true,
source = model.navigation,
serializer = AddToPortfolioRoutes.serializer(),
initialStack = { model.currentStack },
childFactory = { configuration, _ ->
contentChild(configuration)
},
)
private fun onBack() {
if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss()
}
override fun dismiss() {
params.callback.onDismiss()
}
@Composable
override fun BottomSheet() {
AddToPortfolioBottomSheet(
childStack = childStack.subscribeAsState(),
onBack = ::onBack,
onDismiss = ::dismiss,
)
}
private fun contentChild(config: AddToPortfolioRoutes): ComposableContentComponent = when (config) {
AddToPortfolioRoutes.AddToken -> addTokenComponent
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
AddToPortfolioRoutes.TokenActions -> ComposableContentComponent.EMPTY
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
is AddToPortfolioRoutes.NetworkSelector -> ComposableContentComponent.EMPTY
}
@AssistedFactory
interface Factory : AddToPortfolioPreselectedDataComponent.Factory {
override fun create(
context: AppComponentContext,
params: AddToPortfolioPreselectedDataComponent.Params,
): DefaultAddToPortfolioPreselectedDataComponent
}
}

View file

@ -2,10 +2,7 @@ package com.tangem.features.feed.components.market.details.portfolio.add.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.ChooseNetworkModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.TokenActionsModel
import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.*
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -26,6 +23,11 @@ internal interface AddToPortfolioModelModule {
@ClassKey(AddToPortfolioModel::class)
fun addToPortfolioModel(model: AddToPortfolioModel): Model
@Binds
@IntoMap
@ClassKey(AddToPortfolioPreselectedDataModel::class)
fun addToPortfolioPreselectedDataModel(model: AddToPortfolioPreselectedDataModel): Model
@Binds
@IntoMap
@ClassKey(TokenActionsModel::class)

View file

@ -67,7 +67,7 @@ internal class AddToPortfolioModel @Inject constructor(
private val addToPortfolioManager = params.addToPortfolioManager
val portfolioFetcher = addToPortfolioManager.portfolioFetcher
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = addToPortfolioManager.token,
tokenSymbol = addToPortfolioManager.token.symbol,
source = addToPortfolioManager.analyticsParams?.source,
)

View file

@ -0,0 +1,280 @@
package com.tangem.features.feed.components.market.details.portfolio.add.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
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.extensions.resourceReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
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.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.feed.components.market.details.portfolio.add.*
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.feed.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
internal class AddToPortfolioPreselectedDataModel @Inject constructor(
paramsContainer: ParamsContainer,
portfolioFetcherFactory: PortfolioFetcher.Factory,
override val dispatchers: CoroutineDispatcherProvider,
val portfolioSelectorController: PortfolioSelectorController,
private val callbackDelegate: AddToPortfolioFromEarnCallbackDelegate,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
) : Model(), AddTokenComponent.Callbacks by callbackDelegate {
private val params = paramsContainer.require<AddToPortfolioPreselectedDataComponent.Params>()
val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = modelScope,
)
val navigation = StackNavigation<AddToPortfolioRoutes>()
var currentStack = listOf<AddToPortfolioRoutes>(AddToPortfolioRoutes.Empty)
private val _selectedNetwork = MutableStateFlow<SelectedNetwork?>(null)
val selectedNetwork: Flow<SelectedNetwork> = _selectedNetwork.asStateFlow().filterNotNull()
private val _selectedPortfolio = MutableStateFlow<SelectedPortfolio?>(null)
val selectedPortfolio: Flow<SelectedPortfolio> = _selectedPortfolio.asStateFlow().filterNotNull()
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.tokenToAdd.symbol,
source = AnalyticsParam.ScreensSources.Markets.value,
)
init {
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
startAddToPortfolioFlow()
}
@Suppress("LongMethod")
private fun startAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishSuccessFlow(currency: CryptoCurrency, userWalletId: UserWalletId) {
params.callback.onSuccess(addedToken = currency, walletId = userWalletId)
channel.close()
}
val data = createAvailableToAddDataForPreselectedNetwork(params.tokenToAdd.network)
?: return@channelFlow
val isAccountMode = portfolioSelectorController.isAccountModeSync()
val firstSelectedPortfolio = setupPortfolioFlow(data)
.onEach { _selectedPortfolio.value = it }
val firstSelectedNetwork = firstSelectedPortfolio
.map { portfolio -> createSelectedNetwork(network = params.tokenToAdd.network, portfolio = portfolio) }
.filterNotNull()
.onEach { _selectedNetwork.value = it }
val isSinglePortfolio = data.isSinglePortfolio
if (isSinglePortfolio) {
val accountId = data.availableToAddWallets.values.first()
.availableToAddAccounts.values.first()
.account.account.accountId
// force select a portfolio, triggers [selectedPortfolio]
portfolioSelectorController.selectAccount(accountId)
} else {
logAccountSelector(isAccountMode)
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
}
// main flow that combine all require data
val allRequireForAdd = combine(
flow = firstSelectedNetwork,
flow2 = firstSelectedPortfolio,
transform = { a, b -> a to b },
)
// suspend until all required data is selected
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks
.any { it.networkId == selectedNetworkValue.selectedNetwork.networkId }
if (isTokenAlreadyAdded) {
finishSuccessFlow(
currency = selectedNetworkValue.cryptoCurrency,
userWalletId = selectedPortfolioValue.userWallet.walletId,
)
return@channelFlow
}
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId)
}
.catch { throwable ->
Timber.e(throwable)
params.callback.onDismiss()
}
.launchIn(modelScope)
}
private fun logAccountSelector(isAccountMode: Boolean) {
if (isAccountMode) {
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
}
}
private fun minimalTokenMarketParams() = with(params.tokenToAdd) {
TokenMarketParams(
id = id,
name = name,
symbol = symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = BigDecimal.ZERO,
h24Percent = null,
weekPercent = null,
monthPercent = null,
),
imageUrl = null,
)
}
/**
* Creates [AvailableToAddData] for preselected network when token is already added in all networks.
* This allows user to select wallet/account and do smth after it with selected info.
*/
private suspend fun createAvailableToAddDataForPreselectedNetwork(
preSelectedNetwork: TokenMarketInfo.Network,
): AvailableToAddData? {
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null
val availableToOpenWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
val wallet = balance.userWallet
val accounts = balance.accountsBalance.accountStatuses
val availableToAddAccounts = accounts.mapNotNull { accountStatus ->
val accountIndex = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex
}
val cryptoCurrency = getTokenMarketCryptoCurrency(
userWalletId = walletId,
tokenMarketParams = minimalTokenMarketParams(),
network = preSelectedNetwork,
accountIndex = accountIndex,
) ?: return@mapNotNull null
val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = walletId,
currency = cryptoCurrency,
).fold(
ifEmpty = { emptySet() },
ifSome = { setOf(it.status.currency.network) },
)
AvailableToAddAccount(
account = accountStatus,
availableNetworks = setOf(preSelectedNetwork),
addedNetworks = addedNetworks,
)
}.associateBy { it.account.account.accountId }
if (availableToAddAccounts.isEmpty()) return@mapNotNull null
walletId to AvailableToAddWallet(
userWallet = wallet,
accounts = accounts,
availableNetworks = setOf(preSelectedNetwork),
availableToAddAccounts = availableToAddAccounts,
)
}.toMap()
if (availableToOpenWallets.isEmpty()) return null
return AvailableToAddData(availableToAddWallets = availableToOpenWallets)
}
private suspend fun createCryptoCurrency(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
account: AvailableToAddAccount,
): CryptoCurrency? {
val accountIndex = when (val accountStatus = account.account) {
is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex
}
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = minimalTokenMarketParams(),
network = network,
accountIndex = accountIndex,
)
}
private suspend fun createSelectedNetwork(
network: TokenMarketInfo.Network,
portfolio: SelectedPortfolio,
): SelectedNetwork? {
val cryptoCurrency = createCryptoCurrency(
userWallet = portfolio.userWallet,
network = network,
account = portfolio.account,
) ?: return null
return SelectedNetwork(
cryptoCurrency = cryptoCurrency,
selectedNetwork = network,
isAvailableMoreNetwork = false,
)
}
private fun setupPortfolioFlow(data: AvailableToAddData): Flow<SelectedPortfolio> = combine(
flow = portfolioSelectorController.isAccountMode,
flow2 = portfolioSelectorController.selectedAccount,
transform = { isAccountMode, selectedAccountId ->
selectedAccountId ?: return@combine null
val availableToAddWallets =
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
val availableToAddAccount =
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,
account = availableToAddAccount,
isAvailableMorePortfolio = false,
)
},
)
.filterNotNull()
}
@ModelScoped
internal class AddToPortfolioFromEarnCallbackDelegate @Inject constructor() :
AddTokenComponent.Callbacks {
val onTokenAdded = Channel<CryptoCurrencyStatus>()
override fun onChangeNetworkClick() = Unit
override fun onChangePortfolioClick() = Unit
override fun onTokenAdded(status: CryptoCurrencyStatus) {
onTokenAdded.trySend(status)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.feed.components.market.details.portfolio.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM
internal class PortfolioAnalyticsEvent(
@ -10,14 +9,14 @@ internal class PortfolioAnalyticsEvent(
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
data class EventBuilder(
val token: TokenMarketParams,
val tokenSymbol: String,
val source: String?,
) {
fun addToPortfolioClicked() = PortfolioAnalyticsEvent(
event = "Button - Add To Portfolio",
params = mapOf(
"Token" to token.symbol,
"Token" to tokenSymbol,
),
)
@ -35,7 +34,7 @@ internal class PortfolioAnalyticsEvent(
event = "Token Network Selected",
params = mapOf(
"Count" to blockchainNames.size.toString(),
"Token" to token.symbol,
"Token" to tokenSymbol,
"blockchain" to blockchainNames.joinToString(separator = ", "),
),
)
@ -51,7 +50,7 @@ internal class PortfolioAnalyticsEvent(
else -> "error"
},
params = buildMap {
put("Token", token.symbol)
put("Token", tokenSymbol)
source?.let { put("Source", it) }
put("blockchain", blockchainName)
},

View file

@ -81,7 +81,7 @@ internal class MarketsPortfolioModel @Inject constructor(
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
tokenSymbol = params.token.symbol,
source = params.analyticsParams?.source,
)

View file

@ -9,4 +9,7 @@ internal class DefaultFeedFeatureToggle(
override val isFeedEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
override val isEarnBlockEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED")
}

View file

@ -47,10 +47,10 @@ internal class FeedComponentModel @Inject constructor(
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val stateController: FeedStateController,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
private val stateController: FeedStateController,
) : Model() {
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
@ -80,11 +80,9 @@ internal class FeedComponentModel @Inject constructor(
init {
initializeState()
updateCallbacks()
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
}
fetchTrendingNews()
subscribeOnCurrencyUpdate()
loadCharts()
modelScope.launch(dispatchers.default) {
combine(
flow = marketsBatchFlowManager.itemsByOrder,
@ -144,13 +142,23 @@ internal class FeedComponentModel @Inject constructor(
}
}.collect()
}
}
private fun fetchTrendingNews() {
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
}
}
private fun subscribeOnCurrencyUpdate() {
modelScope.launch(dispatchers.default) {
currentAppCurrency.drop(1).collect {
marketsBatchFlowManager.reloadAll()
}
}
}
private fun loadCharts() {
modelScope.launch(dispatchers.default) {
TokenMarketListConfig.Order.entries.forEach { order ->
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->

View file

@ -0,0 +1,44 @@
package com.tangem.features.feed.ui.feed.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.collections.immutable.ImmutableList
internal data class EarnListUM(
val items: ImmutableList<EarnListItemUM>,
val contentState: EarnListContentState,
)
@Immutable
internal data class EarnListItemUM(
val network: TextReference.Str,
val symbol: String,
val tokenName: String,
val currencyIconState: CurrencyIconState,
val earnValue: EarnValueUM,
val earnType: EarnType,
val onItemClick: () -> Unit,
)
@Immutable
internal data class EarnValueUM(
val percent: SerializedBigDecimal,
val earnValueType: EarnValueType,
)
internal enum class EarnType {
Staking, Yield
}
internal enum class EarnValueType {
APR, APY
}
@Immutable
internal sealed interface EarnListContentState {
data object Loading : EarnListContentState
data object Content : EarnListContentState
data class Error(val onRetryClicked: () -> Unit) : EarnListContentState
}