Updated on 2026-08-14
This commit is contained in:
parent
cae7844408
commit
5037fabe32
14 changed files with 602 additions and 122 deletions
|
|
@ -82,5 +82,9 @@
|
|||
{
|
||||
"name": "SWAP_MARKET_LIST_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "EARN_BLOCK_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ package com.tangem.features.feed.entry.featuretoggle
|
|||
|
||||
interface FeedFeatureToggle {
|
||||
val isFeedEnabled: Boolean
|
||||
val isEarnBlockEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue