From eb6d4806e657f4a6c4f01dd1045ca8c001bd58ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Apr 2026 17:03:15 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../addtoportfolio/AddToPortfolioManager.kt | 1 + .../AddToPortfolioPreselectedDataComponent.kt | 38 --- ...tAddToPortfolioPreselectedDataComponent.kt | 91 ------ .../di/AddToPortfolioComponentModule.kt | 7 - .../di/AddToPortfolioModelModule.kt | 6 - .../model/AddToPortfolioModel.kt | 308 ++++++++---------- .../AddToPortfolioPreselectedDataModel.kt | 300 ----------------- .../feed/components/FeedEntryChildFactory.kt | 6 +- .../components/earn/DefaultEarnComponent.kt | 17 +- .../components/feed/DefaultFeedComponent.kt | 14 +- .../components/feed/FeedBottomSheetRoute.kt | 2 - .../features/feed/model/earn/EarnModel.kt | 110 +++++-- .../earn/analytics/EarnAnalyticsEvent.kt | 7 +- .../UpdateMostlyUsedStateTransformer.kt | 6 +- .../statemanager/EarnListBatchFlowManager.kt | 6 +- .../feed/model/feed/FeedComponentModel.kt | 82 +++-- 16 files changed, 291 insertions(+), 710 deletions(-) delete mode 100644 features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt delete mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt delete mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt index 5b53e9abc2..b4d0f52a3f 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt @@ -76,6 +76,7 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal { companion object { val DefaultMarket = Settings(shouldSkipTokenActionsScreen = false) val ChooseToken = Settings(shouldSkipTokenActionsScreen = true) + val Earn = Settings(shouldSkipTokenActionsScreen = true) } } diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt deleted file mode 100644 index 59b4f03918..0000000000 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.commonfeatures.api.addtoportfolio - -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 -import kotlinx.serialization.Serializable - -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, - val analyticsParams: AnalyticsParams, - ) - - data class AnalyticsParams(val source: String) - - interface Callback { - fun onDismiss() - fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) - } - - @Serializable - 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/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt deleted file mode 100644 index 103f0d4ee0..0000000000 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.tangem.features.commonfeatures.impl.addtoportfolio - -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.commonfeatures.api.portfolioselector.PortfolioSelectorComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent -import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioPreselectedDataModel -import com.tangem.features.commonfeatures.impl.addtoportfolio.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 - AddToPortfolioRoutes.UserPortfolio -> 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/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt index c32baf5b2f..020655ca65 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt @@ -2,9 +2,7 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.di import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioComponent -import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioPreselectedDataComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.DefaultAddToPortfolioManager import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.DefaultUserPortfolioComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent @@ -23,11 +21,6 @@ internal interface AddToPortfolioComponentModule { @Binds fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory - @Binds - fun bindAddToPortfolioPreselectedDataComponent( - factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, - ): AddToPortfolioPreselectedDataComponent.Factory - @Binds fun bindUserPortfolioComponentFactory( factory: DefaultUserPortfolioComponent.Factory, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt index a0ab0d1bf4..699b4df576 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt @@ -3,7 +3,6 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel -import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioPreselectedDataModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel @@ -28,11 +27,6 @@ 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/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index 9c2305f233..62226b2e71 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ToastMessage +import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.markets.GetTokenMarketCryptoCurrency import com.tangem.domain.markets.RawMarketToken @@ -60,6 +61,7 @@ internal class AddToPortfolioModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectionResolver: AddToPortfolioInitialSelectionResolver, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, userPortfolioStateControllerFactory: UserPortfolioStateController.Factory, ) : Model(), ChooseNetworkComponent.Callbacks by callbackDelegate, @@ -95,19 +97,13 @@ internal class AddToPortfolioModel @Inject constructor( onTokenSelected = { result -> addToPortfolioManager.onAddedTokenClick(result) }, ) - val featureData: Flow = combineFeatureData() - private val globalSelectedWallet: UserWallet? get() = getSelectedWalletSyncUseCase().getOrNull() .takeIf { it?.isMultiCurrency == true } init { navigation.subscribe { currentStack = it.transformer.invoke(currentStack) } - if (designFeatureToggles.isRedesignEnabled) { - startRedesignAddToPortfolioFlow() - } else { - startLegacyAddToPortfolioFlow() - } + startRedesignAddToPortfolioFlow() } private fun replayMutableSharedFlow() = MutableSharedFlow( @@ -115,132 +111,43 @@ internal class AddToPortfolioModel @Inject constructor( onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - @Suppress("LongMethod") - private fun startLegacyAddToPortfolioFlow() { - channelFlow { - fun finishSuccessFlow(result: AddToPortfolioManager.Result) { - addToPortfolioManager.onSuccessAdded(result) - channel.close() - } - - val featureDataFlow: StateFlow = featureData - .filterIsInstance() - .map { it.availableToAddData } - .distinctUntilChanged() - .stateIn(this) - val isAccountMode = portfolioSelectorController.isAccountModeSync() - - // use snapshot data, looks like we don’t need to remap at runtime - val data = featureDataFlow.value - - // init data flows, emits on user/code selection, updates state holder - val firstSelectedPortfolio = setupPortfolioFlow(data) - .onEach { selectedPortfolio.emit(it) } - val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio) - .onEach { selectedNetwork.emit(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) - } - - val firstPartOfNavigation: Job = firstSelectedPortfolio - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - when { - // force select a network, triggers [selectedNetwork] - isSingleAvailableNetwork -> { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } - // it's important to control root screen, UI depends on it(close/arrow icon) - isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) - else -> navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - .launchIn(this) - - // 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 firstPair = allRequireForAdd.first() - // line of navigation to AddToken screen is finished; cancel the job, select a new root screen - firstPartOfNavigation.cancel() - - val selectedNetworkName = firstPair.first.cryptoCurrency.network.name - analyticsEventHandler.send(event = eventBuilder.popupToConfirm(selectedNetworkName)) - navigation.replaceAll(AddToPortfolioRoutes.AddToken) - - var middleNavigationJob: Job? = null - // handle actions from AddToken screen - callbackDelegate.onChangeNetworkClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changeNetworkNavigationFlow() - .launchIn(this) - val route = routeToNetworkSelector(selectedPortfolio.first()) - navigation.pushNew(route) - } - .launchIn(this) - // handle actions from AddToken screen - callbackDelegate.onChangePortfolioClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) - logAccountSelector(isAccountMode) - navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) - } - .launchIn(this) - - // suspend until token is added - val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() - middleNavigationJob?.cancel() - val selectedPortfolio = selectedPortfolio.first() - analyticsEventHandler.send(eventBuilder.tokenAdded(addedToken.currency.network.name)) - if (!selectedPortfolio.account.account.account.isMainAccount) { - analyticsEventHandler.send(eventBuilder.addToNotMainAccount()) - } - val result = AddToPortfolioManager.Result( - wallet = selectedPortfolio.userWallet, - account = selectedPortfolio.account.account, - addedCurrency = addedToken, - ) - - messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) - - if (addToPortfolioManager.settings.shouldSkipTokenActionsScreen) { - finishSuccessFlow(result) - } else { - setupTokenActionsFlow(selectedPortfolio, addedToken) - .onEach { cryptoCurrencyData -> - tokenActionsData.emit(cryptoCurrencyData) - navigation.replaceAll(AddToPortfolioRoutes.TokenActions) - } - .onEmpty { finishSuccessFlow(result) } - .launchIn(this) - } - - callbackDelegate.onLaterClick.receiveAsFlow().first() - analyticsEventHandler.send(eventBuilder.getTokenLater()) - finishSuccessFlow(result) + private fun lineNavigationFlowToAddTokenScreen( + isAccountMode: Boolean, + data: AvailableToAddData, + firstSelectedPortfolioFlow: Flow, + ): Flow { + 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) } - .catch { throwable -> - TangemLogger.e("Error", throwable) - addToPortfolioManager.onDismiss() + + return firstSelectedPortfolioFlow.onEach { portfolio -> + val isAvailableToAdd = portfolio.account.isAvailableToAdd + val isSingleAvailableNetwork = portfolio.account.isSingleNetwork + when { + // force select an added network, not allowed to add + // but its call AddToPortfolioManager.onAddedTokenClick callback + !isAvailableToAdd -> { + val singleNetwork = portfolio.account.addedMarketNetworks.first() + callbackDelegate.onNetworkSelected(singleNetwork) + } + // force select a network, triggers [selectedNetwork] + isSingleAvailableNetwork -> { + val singleNetwork = portfolio.account.availableToAddNetworks.first() + callbackDelegate.onNetworkSelected(singleNetwork) + } + // it's important to control root screen, UI depends on it(close/arrow icon) + isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) + else -> navigation.pushNew(routeToNetworkSelector(portfolio)) } - .launchIn(modelScope) + } } @Suppress("LongMethod") @@ -250,6 +157,10 @@ internal class AddToPortfolioModel @Inject constructor( addToPortfolioManager.onSuccessAdded(result) channel.close() } + fun finishOnAddedTokenClick(result: AddToPortfolioManager.Result) { + addToPortfolioManager.onAddedTokenClick(result) + channel.close() + } fun finishDismissFlow() { addToPortfolioManager.onDismiss() @@ -259,45 +170,85 @@ internal class AddToPortfolioModel @Inject constructor( val tokenMarketParams = paramsSnapshot.token val launchMode = paramsSnapshot.launchMode - val initialData = featureData + val isAccountMode = portfolioSelectorController.isAccountModeSync() + val initialData: AvailableToAddData = addToPortfolioManager.state .filterIsInstance() .map { it.availableToAddData } .first() - if (launchMode is AddToPortfolioManager.LaunchMode.ViaUserPortfolio && + setupPortfolioSelector(initialData, launchMode) + + val shouldShowUserPortfolio = designFeatureToggles.isRedesignEnabled && + launchMode is AddToPortfolioManager.LaunchMode.ViaUserPortfolio && initialData.hasAnyAddedCurrency(tokenMarketParams.id) - ) { + + if (shouldShowUserPortfolio) { // suspend, must prepare UM before navigate to UserPortfolio - userPortfolioStateController.updateAndWaitNotNullState(initialData, tokenMarketParams.id) + userPortfolioStateController.updateAndWaitNotNullState( + allAvailableData = initialData, + rawCurrencyId = tokenMarketParams.id, + ) navigation.replaceAll(AddToPortfolioRoutes.UserPortfolio) callbackDelegate.onContinueFromUserPortfolio.receiveAsFlow().first() } - val selection = selectionResolver.resolve( - availableToAddData = initialData, - orderedNetworks = paramsSnapshot.networks, - selectedWallet = globalSelectedWallet, - tokenParams = tokenMarketParams, - ) ?: run { - finishDismissFlow() - return@channelFlow + + val initialSelection: AddToPortfolioInitialSelectionResolver.InitialSelection? = when (launchMode) { + AddToPortfolioManager.LaunchMode.Preselected -> null + is AddToPortfolioManager.LaunchMode.ViaUserPortfolio, + AddToPortfolioManager.LaunchMode.DirectAdd, + -> if (designFeatureToggles.isRedesignEnabled) { + getInitialSelection(initialData) + } else { + null + } } - val isAccountMode = portfolioSelectorController.isAccountModeSync() - portfolioSelectorController.selectAccount(selection.account.account.accountId) + val firstSelectedPortfolioFlow: Flow = + setupPortfolioFlow(initialData).onEach { selectedPortfolio.emit(it) } + val firstSelectedNetworkFlow: Flow = + setupNetworkFlow(firstSelectedPortfolioFlow).onEach { selectedNetwork.emit(it) } - val firstSelectedPortfolio = SelectedPortfolio( - isAccountMode = isAccountMode, - userWallet = selection.userWallet, - account = selection.account, - isAvailableMorePortfolio = !initialData.isSinglePortfolio, + // main flow that combine all require data + val allRequireForAdd = combine( + flow = firstSelectedNetworkFlow, + flow2 = firstSelectedPortfolioFlow, + transform = { a, b -> a to b }, ) - val firstSelectedNetwork = selection.toSelectedNetwork() ?: run { - finishDismissFlow() - return@channelFlow + + var firstPartOfNavigation: Job? = null + + if (initialSelection != null) { + launch { + portfolioSelectorController.selectAccount(initialSelection.account.account.accountId) + callbackDelegate.onNetworkSelected.send(initialSelection.network) + } + } else { + firstPartOfNavigation = lineNavigationFlowToAddTokenScreen( + isAccountMode = isAccountMode, + data = initialData, + firstSelectedPortfolioFlow = firstSelectedPortfolioFlow, + ).launchIn(this) } - selectedPortfolio.emit(firstSelectedPortfolio) - selectedNetwork.emit(firstSelectedNetwork) + // suspend until all required data is selected + val (firstSelectedNetwork, firstSelectedPortfolio) = allRequireForAdd.first() + // line navigation to AddToken screen is finished; cancel the job if exists + firstPartOfNavigation?.cancel() + + val alreadyAddedToken = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = firstSelectedPortfolio.userWallet.walletId, + currency = firstSelectedNetwork.cryptoCurrency, + ).getOrNull() + + if (alreadyAddedToken != null) { + val result = AddToPortfolioManager.Result( + wallet = firstSelectedPortfolio.userWallet, + account = firstSelectedPortfolio.account.account, + addedCurrency = alreadyAddedToken.status, + ) + finishOnAddedTokenClick(result) + return@channelFlow + } val selectedNetworkName = firstSelectedNetwork.cryptoCurrency.network.name analyticsEventHandler.send(event = eventBuilder.popupToConfirm(selectedNetworkName)) @@ -317,11 +268,15 @@ internal class AddToPortfolioModel @Inject constructor( callbackDelegate.onChangePortfolioClick.receiveAsFlow() .onEach { middleNavigationJob?.cancel() - middleNavigationJob = changePortfolioNavigationNewFlow( - data = initialData, - orderedNetworks = paramsSnapshot.networks, - tokenParams = tokenMarketParams, - ).launchIn(this) + middleNavigationJob = if (designFeatureToggles.isRedesignEnabled) { + changePortfolioNavigationNewFlow( + data = initialData, + orderedNetworks = paramsSnapshot.networks, + tokenParams = tokenMarketParams, + ) + } else { + changePortfolioNavigationFlow(initialData) + }.launchIn(this) logAccountSelector(isAccountMode) navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) } @@ -362,6 +317,17 @@ internal class AddToPortfolioModel @Inject constructor( .launchIn(modelScope) } + private suspend fun getInitialSelection( + initialData: AvailableToAddData, + ): AddToPortfolioInitialSelectionResolver.InitialSelection? { + return selectionResolver.resolve( + availableToAddData = initialData, + orderedNetworks = paramsSnapshot.networks, + selectedWallet = globalSelectedWallet, + tokenParams = paramsSnapshot.token, + ) + } + private fun logAccountSelector(isAccountMode: Boolean) { if (isAccountMode) { analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) @@ -523,19 +489,21 @@ internal class AddToPortfolioModel @Inject constructor( return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio) } - private fun combineFeatureData() = addToPortfolioManager.state.onEach { state -> - when (state) { - is AddToPortfolioManager.State.Ready -> - portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> - val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] + private fun setupPortfolioSelector(data: AvailableToAddData, launchMode: AddToPortfolioManager.LaunchMode) { + portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> + when (launchMode) { + AddToPortfolioManager.LaunchMode.Preselected -> return@isEnabled true + AddToPortfolioManager.LaunchMode.DirectAdd, + is AddToPortfolioManager.LaunchMode.ViaUserPortfolio, + -> { + val availableWallet = data.availableToAddWallets[userWallet.walletId] ?: return@isEnabled false - val isAvailableAccount = - availableWallet.availableToAddAccounts[accountStatus.account.accountId] - ?.isAvailableToAdd == true + val isAvailableAccount = availableWallet + .availableToAddAccounts[accountStatus.account.accountId] + ?.isAvailableToAdd == true return@isEnabled isAvailableAccount } - AddToPortfolioManager.State.Loading, - -> Unit + } } } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt deleted file mode 100644 index d1e84ecc71..0000000000 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt +++ /dev/null @@ -1,300 +0,0 @@ -package com.tangem.features.commonfeatures.impl.addtoportfolio.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.RawMarketToken -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.account.filterCryptoPortfolio -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher -import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController -import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.* -import com.tangem.features.commonfeatures.impl.R -import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.EarnAnalyticsEvent -import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.logging.TangemLogger -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import javax.inject.Inject -import kotlin.collections.mapNotNull - -@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, - category = 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 = getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = selectedPortfolioValue.userWallet.walletId, - currency = selectedNetworkValue.cryptoCurrency, - ).isSome() - - if (isTokenAlreadyAdded) { - finishSuccessFlow( - currency = selectedNetworkValue.cryptoCurrency, - userWalletId = selectedPortfolioValue.userWallet.walletId, - ) - return@channelFlow - } - - sendAddTokenOpenedAnalytics(selectedNetworkValue.cryptoCurrency) - navigation.replaceAll(AddToPortfolioRoutes.AddToken) - val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() - messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) - sendSuccessAddedAnalytics(addedToken.currency) - finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId) - } - .catch { throwable -> - TangemLogger.e("Error", throwable) - params.callback.onDismiss() - } - .launchIn(modelScope) - } - - private fun logAccountSelector(isAccountMode: Boolean) { - if (isAccountMode) { - analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) - } - } - - private fun minimalTokenMarketParams() = with(params.tokenToAdd) { - RawMarketToken( - id = id, - name = name, - symbol = symbol, - ) - } - - /** - * Creates [com.tangem.features.commonfeatures.api.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 availableToAddInWallets = portfolioData.balances.mapNotNull { (walletId, balance) -> - val wallet = balance.userWallet - val accounts = balance.accountsBalance.accountStatuses.filterCryptoPortfolio() - - 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 (availableToAddInWallets.isEmpty()) return null - - return AvailableToAddData(availableToAddWallets = availableToAddInWallets) - } - - 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 - is AccountStatus.Payment -> return null - } - 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() - - private fun sendSuccessAddedAnalytics(cryptoCurrency: CryptoCurrency) { - analyticsEventHandler.send( - EarnAnalyticsEvent.TokenAdded( - tokenSymbol = cryptoCurrency.symbol, - blockchain = cryptoCurrency.network.name, - ), - ) - } - - private fun sendAddTokenOpenedAnalytics(cryptoCurrency: CryptoCurrency) { - analyticsEventHandler.send( - EarnAnalyticsEvent.AddTokenScreenOpened( - tokenSymbol = cryptoCurrency.symbol, - blockchain = cryptoCurrency.network.name, - source = params.analyticsParams.source, - ), - ) - } -} - -@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/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index cb3ad4c829..768445bbd6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -12,7 +12,6 @@ import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent @@ -31,7 +30,6 @@ internal class FeedEntryChildFactory @Inject constructor( private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, - private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory, private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory, private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles, private val designFeatureToggles: DesignFeatureToggles, @@ -132,7 +130,7 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultFeedComponent( appComponentContext = appComponentContext, params = FeedParams(feedClickIntents = feedEntryClickIntents), - addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, + addToPortfolioComponentFactory = addToPortfolioComponentFactory, promoBannersBlockComponentFactory = promoBannersBlockComponentFactory, newPromoBannersFeatureToggles = newPromoBannersFeatureToggles, ) @@ -144,7 +142,7 @@ internal class FeedEntryChildFactory @Inject constructor( onBackClick = onBackClicked, onSearchClicked = feedEntryClickIntents::openSearch, ), - addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, + addToPortfolioComponentFactory = addToPortfolioComponentFactory, ) } is Child.Search -> DefaultSearchComponent( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index 8ddecd2dc6..bdef4f7ffe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -33,9 +33,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent import dev.chrisbanes.haze.HazeProgressive @@ -43,7 +44,7 @@ import dev.chrisbanes.haze.HazeProgressive internal class DefaultEarnComponent( appComponentContext: AppComponentContext, private val params: Params, - private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val earnModel = getOrCreateModel(params = params) @@ -129,10 +130,14 @@ internal class DefaultEarnComponent( is FeedBottomSheetRoute.AddToPortfolio -> { addToPortfolioComponentFactory.create( context = childByContext(componentContext), - params = AddToPortfolioPreselectedDataComponent.Params( - tokenToAdd = config.tokenToAdd, - callback = earnModel.addToPortfolioCallback, - analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = when { + config.source == EarnSource.BEST_OPPORTUNITIES_SOURCE.value -> + earnModel.addBestOpportunitiesPortfolioManager + config.source == EarnSource.MOSTLY_USED_SOURCE.value -> + earnModel.addMostlyUsedPortfolioManager + else -> error("Unknown source: ${config.source}") + }, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index cb39402dc7..21ebee8140 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -11,7 +11,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot -import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -20,8 +19,7 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent.Params +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList @@ -32,7 +30,7 @@ import com.tangem.features.promobanners.api.PromoBannersBlockComponent internal class DefaultFeedComponent( appComponentContext: AppComponentContext, private val params: FeedParams, - private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory, private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { @@ -102,12 +100,8 @@ internal class DefaultFeedComponent( is FeedBottomSheetRoute.AddToPortfolio -> { addToPortfolioComponentFactory.create( context = childByContext(componentContext), - params = Params( - tokenToAdd = config.tokenToAdd, - callback = feedComponentModel.addToPortfolioCallback, - analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams( - AnalyticsParam.ScreensSources.Markets.value, - ), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = feedComponentModel.addToPortfolioManager, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt index ecbaa6c87a..fc72c74f86 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -2,12 +2,10 @@ package com.tangem.features.feed.components.feed import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent internal sealed interface FeedBottomSheetRoute { data class AddToPortfolio( - val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd, val source: String, ) : FeedBottomSheetRoute diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 9ecb1ab5f4..d795a1289d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -17,17 +17,18 @@ import com.tangem.domain.earn.model.EarnFilter import com.tangem.domain.earn.model.EarnFilterNetwork import com.tangem.domain.earn.model.EarnFilterType import com.tangem.domain.earn.usecase.* +import com.tangem.domain.markets.RawMarketToken import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.earn.EarnTokenWithCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter @@ -48,7 +49,7 @@ import javax.inject.Inject @Stable @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class EarnModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -62,6 +63,7 @@ internal class EarnModel @Inject constructor( private val appRouter: AppRouter, private val stateController: EarnStateController, private val analyticsEventHandler: AnalyticsEventHandler, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, ) : Model() { private val params = paramsContainer.require() @@ -82,21 +84,24 @@ internal class EarnModel @Inject constructor( dispatchers = dispatchers, ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { - bottomSheetNavigation.dismiss() - appRouter.push( - AppRoute.CurrencyDetails( - userWalletId = walletId, - currency = addedToken, - ), - ) - } + val addBestOpportunitiesPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( + scope = modelScope, + settings = AddToPortfolioManager.Settings.Earn, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.BEST_OPPORTUNITIES_SOURCE.value), + ).apply { + updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) } + val addMostlyUsedPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( + scope = modelScope, + settings = AddToPortfolioManager.Settings.Earn, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.MOSTLY_USED_SOURCE.value), + ).apply { + updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) + } + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val state: StateFlow get() = stateController.uiState @@ -107,6 +112,39 @@ internal class EarnModel @Inject constructor( subscribeOnNetworks() subscribeOnBatchFlow() subscribeToMostlyUsed() + + addBestOpportunitiesPortfolioManager.onDismiss.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .launchIn(modelScope) + addBestOpportunitiesPortfolioManager.onSuccessAdded.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + addBestOpportunitiesPortfolioManager.onAddedTokenClick.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + + addMostlyUsedPortfolioManager.onDismiss.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .launchIn(modelScope) + addMostlyUsedPortfolioManager.onSuccessAdded.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + addMostlyUsedPortfolioManager.onAddedTokenClick.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + } + + private fun openCurrencyDetails(result: AddToPortfolioManager.Result) { + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = result.wallet.walletId, + currency = result.addedCurrency.currency, + ), + ) } private fun subscribeOnBatchFlow() { @@ -260,30 +298,36 @@ internal class EarnModel @Inject constructor( } } - private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency, source: String) { + private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency, source: EarnSource) { analyticsEventHandler.send( EarnAnalyticsEvent.OpportunitySelected( tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol, blockchain = earnTokenWithCurrency.cryptoCurrency.network.name, - source = source, + source = source.value, ), ) - bottomSheetNavigation.activate( - FeedBottomSheetRoute.AddToPortfolio( - tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( - network = TokenMarketInfo.Network( - networkId = earnTokenWithCurrency.earnToken.networkId, - isExchangeable = false, - contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, - decimalCount = earnTokenWithCurrency.earnToken.decimalCount, - ), - id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), - name = earnTokenWithCurrency.earnToken.tokenName, - symbol = earnTokenWithCurrency.earnToken.tokenSymbol, - ), - source = source, - ), + val token = RawMarketToken( + id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), + name = earnTokenWithCurrency.earnToken.tokenName, + symbol = earnTokenWithCurrency.earnToken.tokenSymbol, ) + val network = TokenMarketInfo.Network( + networkId = earnTokenWithCurrency.earnToken.networkId, + isExchangeable = false, + contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, + decimalCount = earnTokenWithCurrency.earnToken.decimalCount, + ) + when (source) { + EarnSource.BEST_OPPORTUNITIES_SOURCE -> addBestOpportunitiesPortfolioManager.apply { + setTokenParams(token) + setTokenNetworks(listOf(network)) + } + EarnSource.MOSTLY_USED_SOURCE -> addMostlyUsedPortfolioManager.apply { + setTokenParams(token) + setTokenNetworks(listOf(network)) + } + } + bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(source.value)) } private fun onTypeFilterOptionSelected(type: EarnFilterType) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt index 5369759688..2925791d66 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -62,9 +62,10 @@ internal sealed class EarnAnalyticsEvent( ) } -internal const val BEST_OPPORTUNITIES_SOURCE = "Best Opportunity" -internal const val MOSTLY_USED_SOURCE = "Mostly Used" - +internal enum class EarnSource(val value: String) { + BEST_OPPORTUNITIES_SOURCE("Best Opportunity"), + MOSTLY_USED_SOURCE("Mostly Used"), +} internal enum class FilterNetworkAnalytic(val value: String) { ALL_NETWORKS("All Networks"), MY_NETWORKS("My Networks"), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt index a3a8317268..e2031ca914 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.feed.model.earn.state.transformers import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.domain.models.earn.EarnTopToken import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter -import com.tangem.features.feed.model.earn.analytics.MOSTLY_USED_SOURCE +import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.earn.state.EarnUM import kotlinx.collections.immutable.toPersistentList @@ -11,12 +11,12 @@ import java.math.BigDecimal internal class UpdateMostlyUsedStateTransformer( private val earnResult: EarnTopToken?, - private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, + private val onItemClick: (EarnTokenWithCurrency, source: EarnSource) -> Unit, private val onRetryClick: () -> Unit, ) : EarnUMTransformer { private val converter = EarnTokenWithCurrencyToListItemUMConverter( - onItemClick = { token -> onItemClick(token, MOSTLY_USED_SOURCE) }, + onItemClick = { token -> onItemClick(token, EarnSource.MOSTLY_USED_SOURCE) }, ) override fun transform(prevState: EarnUM): EarnUM { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt index c241d58af4..e762de7360 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -5,7 +5,7 @@ import com.tangem.domain.earn.model.EarnTokensListConfig import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter -import com.tangem.features.feed.model.earn.analytics.BEST_OPPORTUNITIES_SOURCE +import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.ui.earn.state.EarnListItemUM import com.tangem.pagination.BatchAction import com.tangem.pagination.PaginationStatus @@ -21,13 +21,13 @@ import kotlinx.coroutines.launch internal class EarnListBatchFlowManager( getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, private val configProvider: Provider, - private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, + private val onItemClick: (EarnTokenWithCurrency, source: EarnSource) -> Unit, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { private val actionsFlow = MutableSharedFlow>() private val converter = EarnTokenWithCurrencyToListItemUMConverter( - onItemClick = { onItemClick(it, BEST_OPPORTUNITIES_SOURCE) }, + onItemClick = { onItemClick(it, EarnSource.BEST_OPPORTUNITIES_SOURCE) }, ) private val batchFlow = getEarnTokensBatchFlowUseCase( 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 b1d45c9693..7a7c71ccc5 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 @@ -20,18 +20,14 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.earn.usecase.FetchTopEarnTokensUseCase import com.tangem.domain.earn.usecase.GetTopEarnTokensUseCase -import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.markets.* import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.earn.EarnTokenWithCurrency -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent @@ -66,6 +62,7 @@ internal class FeedComponentModel @Inject constructor( private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, private val appRouter: AppRouter, private val designFeatureToggles: DesignFeatureToggles, + addToPortfolioManagerFactory: AddToPortfolioManager.Factory, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, @@ -90,21 +87,16 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { - bottomSheetNavigation.dismiss() - appRouter.push( - AppRoute.CurrencyDetails( - userWalletId = walletId, - currency = addedToken, - ), - ) - } + val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( + scope = modelScope, + settings = AddToPortfolioManager.Settings.Earn, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = AnalyticsParam.ScreensSources.Markets.value), + ).apply { + updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) } + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val state: StateFlow get() = stateController.uiState @@ -118,6 +110,27 @@ internal class FeedComponentModel @Inject constructor( fetchCharts() subscribeOnCurrencyUpdate() subscribeOnDataState() + + addToPortfolioManager.onDismiss.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .launchIn(modelScope) + addToPortfolioManager.onSuccessAdded.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + addToPortfolioManager.onAddedTokenClick.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(modelScope) + } + + private fun openCurrencyDetails(result: AddToPortfolioManager.Result) { + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = result.wallet.walletId, + currency = result.addedCurrency.currency, + ), + ) } private fun subscribeOnDataState() { @@ -407,22 +420,23 @@ internal class FeedComponentModel @Inject constructor( source = AnalyticsParam.ScreensSources.Markets.value, ), ) - bottomSheetNavigation.activate( - FeedBottomSheetRoute.AddToPortfolio( - tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( - network = TokenMarketInfo.Network( - networkId = earnTokenWithCurrency.earnToken.networkId, - isExchangeable = false, - contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, - decimalCount = earnTokenWithCurrency.earnToken.decimalCount, - ), - id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), - name = earnTokenWithCurrency.earnToken.tokenName, - symbol = earnTokenWithCurrency.earnToken.tokenSymbol, - ), - source = AnalyticsParam.ScreensSources.Markets.value, - ), + val token = RawMarketToken( + id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), + name = earnTokenWithCurrency.earnToken.tokenName, + symbol = earnTokenWithCurrency.earnToken.tokenSymbol, ) + val network = TokenMarketInfo.Network( + networkId = earnTokenWithCurrency.earnToken.networkId, + isExchangeable = false, + contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, + decimalCount = earnTokenWithCurrency.earnToken.decimalCount, + ) + val route = FeedBottomSheetRoute.AddToPortfolio(AnalyticsParam.ScreensSources.Markets.value) + addToPortfolioManager.apply { + setTokenParams(token) + setTokenNetworks(listOf(network)) + } + bottomSheetNavigation.activate(route) } private fun handleEarnPageOpenClicked() {