diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 5dc9541240..cc6cdf5665 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -82,5 +82,9 @@ { "name": "SWAP_MARKET_LIST_ENABLED", "version": "undefined" + }, + { + "name": "EARN_BLOCK_ENABLED", + "version": "undefined" } ] diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt new file mode 100644 index 0000000000..713ce93cf1 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt index c04c556450..72d5656bad 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt @@ -2,4 +2,5 @@ package com.tangem.features.feed.entry.featuretoggle interface FeedFeatureToggle { val isFeedEnabled: Boolean + val isEarnBlockEnabled: Boolean } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt new file mode 100644 index 0000000000..8c6aba3001 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt @@ -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>, + 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( + 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, + 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, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt index edd7f01ef5..8f7dfb79a1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt @@ -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( - 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, - 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, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt new file mode 100644 index 0000000000..95efde9298 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt index b60d84f099..60b3f51d91 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt @@ -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) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index 72b3e4b07b..b31e57f8b0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -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, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt new file mode 100644 index 0000000000..b673414488 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -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() + + val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), + scope = modelScope, + ) + + val navigation = StackNavigation() + var currentStack = listOf(AddToPortfolioRoutes.Empty) + + private val _selectedNetwork = MutableStateFlow(null) + val selectedNetwork: Flow = _selectedNetwork.asStateFlow().filterNotNull() + + private val _selectedPortfolio = MutableStateFlow(null) + val selectedPortfolio: Flow = _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 { + 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 = 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() + + override fun onChangeNetworkClick() = Unit + + override fun onChangePortfolioClick() = Unit + + override fun onTokenAdded(status: CryptoCurrencyStatus) { + onTokenAdded.trySend(status) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index 87ea980421..b75d67abf4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -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) }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt index 1715e60e47..e23f24f116 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt @@ -81,7 +81,7 @@ internal class MarketsPortfolioModel @Inject constructor( private val params = paramsContainer.require() private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = params.token, + tokenSymbol = params.token.symbol, source = params.analyticsParams?.source, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt index c15096b261..38b3458572 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt @@ -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") } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 769c975519..8c1774fcf8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -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() @@ -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 -> diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt new file mode 100644 index 0000000000..f703b958c0 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt @@ -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, + 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 +} \ No newline at end of file