Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-28 17:03:15 +04:00
parent 596cbb40cb
commit eb6d4806e6
16 changed files with 291 additions and 710 deletions

View file

@ -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
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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<AddToPortfolioManager.State> = 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 <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
@ -115,132 +111,43 @@ internal class AddToPortfolioModel @Inject constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
@Suppress("LongMethod")
private fun startLegacyAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishSuccessFlow(result: AddToPortfolioManager.Result) {
addToPortfolioManager.onSuccessAdded(result)
channel.close()
}
val featureDataFlow: StateFlow<AvailableToAddData> = featureData
.filterIsInstance<AddToPortfolioManager.State.Ready>()
.map { it.availableToAddData }
.distinctUntilChanged()
.stateIn(this)
val isAccountMode = portfolioSelectorController.isAccountModeSync()
// use snapshot data, looks like we dont 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<SelectedPortfolio>,
): Flow<SelectedPortfolio> {
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<AddToPortfolioManager.State.Ready>()
.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<SelectedPortfolio> =
setupPortfolioFlow(initialData).onEach { selectedPortfolio.emit(it) }
val firstSelectedNetworkFlow: Flow<SelectedNetwork> =
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
}
}
}

View file

@ -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<AddToPortfolioPreselectedDataComponent.Params>()
val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = modelScope,
)
val navigation = StackNavigation<AddToPortfolioRoutes>()
var currentStack = listOf<AddToPortfolioRoutes>(AddToPortfolioRoutes.Empty)
private val _selectedNetwork = MutableStateFlow<SelectedNetwork?>(null)
val selectedNetwork: Flow<SelectedNetwork> = _selectedNetwork.asStateFlow().filterNotNull()
private val _selectedPortfolio = MutableStateFlow<SelectedPortfolio?>(null)
val selectedPortfolio: Flow<SelectedPortfolio> = _selectedPortfolio.asStateFlow().filterNotNull()
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.tokenToAdd.symbol,
source = AnalyticsParam.ScreensSources.Markets.value,
category = AnalyticsParam.ScreensSources.Markets.value,
)
init {
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
startAddToPortfolioFlow()
}
@Suppress("LongMethod")
private fun startAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishSuccessFlow(currency: CryptoCurrency, userWalletId: UserWalletId) {
params.callback.onSuccess(addedToken = currency, walletId = userWalletId)
channel.close()
}
val data = createAvailableToAddDataForPreselectedNetwork(params.tokenToAdd.network)
?: return@channelFlow
val isAccountMode = portfolioSelectorController.isAccountModeSync()
val firstSelectedPortfolio = setupPortfolioFlow(data)
.onEach { _selectedPortfolio.value = it }
val firstSelectedNetwork = firstSelectedPortfolio
.map { portfolio -> createSelectedNetwork(network = params.tokenToAdd.network, portfolio = portfolio) }
.filterNotNull()
.onEach { _selectedNetwork.value = it }
val isSinglePortfolio = data.isSinglePortfolio
if (isSinglePortfolio) {
val accountId = data.availableToAddWallets.values.first()
.availableToAddAccounts.values.first()
.account.account.accountId
// force select a portfolio, triggers [selectedPortfolio]
portfolioSelectorController.selectAccount(accountId)
} else {
logAccountSelector(isAccountMode)
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
}
// main flow that combine all require data
val allRequireForAdd = combine(
flow = firstSelectedNetwork,
flow2 = firstSelectedPortfolio,
transform = { a, b -> a to b },
)
// suspend until all required data is selected
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
val isTokenAlreadyAdded = 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<SelectedPortfolio> = combine(
flow = portfolioSelectorController.isAccountMode,
flow2 = portfolioSelectorController.selectedAccount,
transform = { isAccountMode, selectedAccountId ->
selectedAccountId ?: return@combine null
val availableToAddWallets =
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
val availableToAddAccount =
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,
account = availableToAddAccount,
isAvailableMorePortfolio = false,
)
},
)
.filterNotNull()
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<CryptoCurrencyStatus>()
override fun onChangeNetworkClick() = Unit
override fun onChangePortfolioClick() = Unit
override fun onTokenAdded(status: CryptoCurrencyStatus) {
onTokenAdded.trySend(status)
}
}