Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-28 13:07:24 +04:00
parent a44f392d0a
commit 5315e04487
21 changed files with 199 additions and 177 deletions

View file

@ -247,7 +247,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun createCryptoCurrency(
userWalletId: UserWalletId,
token: TokenMarketParams,
token: RawMarketToken,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex?,
): CryptoCurrency? {

View file

@ -0,0 +1,12 @@
package com.tangem.domain.markets
import com.tangem.domain.models.currency.CryptoCurrency
/**
* minimal token info for add-to-portfolio flow
*/
data class RawMarketToken(
val id: CryptoCurrency.RawID,
val name: String,
val symbol: String,
)

View file

@ -10,7 +10,7 @@ class GetTokenMarketCryptoCurrency(
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
tokenMarketParams: TokenMarketParams,
tokenMarketParams: RawMarketToken,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex,
): CryptoCurrency? {

View file

@ -42,7 +42,7 @@ interface MarketsTokenRepository {
suspend fun createCryptoCurrency(
userWalletId: UserWalletId,
token: TokenMarketParams,
token: RawMarketToken,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex? = null,
): CryptoCurrency?

View file

@ -1,13 +1,13 @@
package com.tangem.features.commonfeatures.api.addtoportfolio
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow
@ -21,11 +21,22 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
val onSuccessAdded: Channel<Result>
val onAddedTokenClick: Channel<Result>
val portfolioFetcher: PortfolioFetcher
val state: StateFlow<State>
/**
* default is [LaunchMode.DirectAdd]
*/
fun updateLaunchMode(launchMode: LaunchMode)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
fun setTokenParams(token: TokenMarketParams)
fun setTokenParams(token: RawMarketToken)
fun setTokenParams(token: TokenMarketParams) = setTokenParams(
RawMarketToken(
id = token.id,
name = token.name,
symbol = token.symbol,
),
)
sealed interface State {
data object Loading : State
@ -36,7 +47,15 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
}
@Serializable
data class AnalyticsParams(val source: String?)
data class AnalyticsParams(
val source: String?,
val category: String = CategoryDefault,
) {
companion object {
const val CategoryDefault = "Markets / Chart"
const val CategoryEarn = "Earn"
}
}
interface Factory {
fun create(scope: CoroutineScope, settings: Settings, analyticsParams: AnalyticsParams): AddToPortfolioManager
@ -44,7 +63,8 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
sealed interface LaunchMode {
data object DirectAdd : LaunchMode
data class ViaUserPortfolio(val rawCurrencyId: CryptoCurrency.RawID) : LaunchMode
data object Preselected : LaunchMode
data object ViaUserPortfolio : LaunchMode
}
/**
@ -52,7 +72,6 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
*/
data class Settings(
val shouldSkipTokenActionsScreen: Boolean = false,
val launchMode: LaunchMode = LaunchMode.DirectAdd,
) {
companion object {
val DefaultMarket = Settings(shouldSkipTokenActionsScreen = false)
@ -64,7 +83,11 @@ interface AddToPortfolioManager : AddToPortfolioManagerInternal {
* Mutable parameters
* Updates may trigger reload [State]
*/
data class Params(val networks: List<TokenMarketInfo.Network>, val token: TokenMarketParams)
data class Params(
val networks: List<TokenMarketInfo.Network>,
val token: RawMarketToken,
val launchMode: LaunchMode,
)
data class Result(
val wallet: UserWallet,
@ -80,8 +103,10 @@ interface AddToPortfolioManagerInternal {
val paramsFlow: SharedFlow<AddToPortfolioManager.Params>
val settings: AddToPortfolioManager.Settings
val analyticsParams: AnalyticsParams
val portfolioFetcher: PortfolioFetcher
suspend fun token(): TokenMarketParams = paramsFlow.first().token
suspend fun params(): AddToPortfolioManager.Params = paramsFlow.first()
suspend fun token(): RawMarketToken = params().token
fun onDismiss()
fun onSuccessAdded(result: AddToPortfolioManager.Result)

View file

@ -45,7 +45,7 @@ internal class AddTokenComponent @AssistedInject constructor(
}
data class Params(
val eventBuilder: Flow<PortfolioAnalyticsEvent.EventBuilder>,
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
val selectedPortfolio: Flow<SelectedPortfolio>,
val selectedNetwork: Flow<SelectedNetwork>,
val callbacks: Callbacks,

View file

@ -12,7 +12,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
@ -37,29 +36,33 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create(
context = child("portfolioSelectorComponent"),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher,
portfolioFetcher = model.addToPortfolioManager.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 addTokenComponent: AddTokenComponent by lazy {
addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = AddTokenComponent.Params(
eventBuilder = model.eventBuilder,
callbacks = model,
selectedPortfolio = model.selectedPortfolio,
selectedNetwork = model.selectedNetwork,
),
)
}
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create(
context = child("tokenActionsComponent"),
params = TokenActionsComponent.Params(
eventBuilder = model.eventBuilder,
callbacks = model,
data = model.tokenActionsData,
),
)
private val tokenActionsComponent: TokenActionsComponent by lazy {
tokenActionsComponentFactory.create(
context = child("tokenActionsComponent"),
params = TokenActionsComponent.Params(
eventBuilder = model.eventBuilder,
callbacks = model,
data = model.tokenActionsData,
),
)
}
private val childStack = childStack(
key = "addToPortfolioStack",
@ -106,16 +109,13 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
}
private fun createUserPortfolioComponent(componentContext: ComponentContext): ComposableContentComponent {
return when (model.addToPortfolioManager.settings.launchMode) {
AddToPortfolioManager.LaunchMode.DirectAdd -> ComposableContentComponent.EMPTY
is AddToPortfolioManager.LaunchMode.ViaUserPortfolio -> userPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = UserPortfolioComponent.Params(
uiState = model.userPortfolioStateController.uiState,
callbacks = model,
),
)
}
return userPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = UserPortfolioComponent.Params(
uiState = model.userPortfolioStateController.uiState,
callbacks = model,
),
)
}
@AssistedFactory

View file

@ -16,7 +16,6 @@ import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfol
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.flowOf
internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@ -38,7 +37,7 @@ internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject con
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = AddTokenComponent.Params(
eventBuilder = flowOf(model.eventBuilder),
eventBuilder = model.eventBuilder,
callbacks = model,
selectedPortfolio = model.selectedPortfolio,
selectedNetwork = model.selectedNetwork,

View file

@ -72,7 +72,7 @@ internal class TokenActionsComponent @AssistedInject constructor(
)
data class Params(
val eventBuilder: Flow<PortfolioAnalyticsEvent.EventBuilder>,
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
val data: Flow<CryptoCurrencyData>,
val callbacks: Callbacks,
)

View file

@ -2,34 +2,41 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.analytics
import com.tangem.common.ui.markets.action.TokenActionsBSContentUM
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
// todo swap unify with EarnAnalyticsEvent, AddToPortfolioFlow
internal class PortfolioAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
category: String,
) : AnalyticsEvent(category = category, event = event, params = params) {
data class EventBuilder(
val tokenSymbol: String,
val source: String?,
val category: String,
) {
fun popupToChooseAccount() = PortfolioAnalyticsEvent(
event = "Choose Account Opened",
category = category,
params = buildMap {
if (source != null) put("Source", source)
if (source != null) put(AnalyticsParam.SOURCE, source)
},
)
fun popupToConfirm() = PortfolioAnalyticsEvent(
fun popupToConfirm(blockchain: String) = PortfolioAnalyticsEvent(
event = "Add Token Screen Opened",
category = category,
params = buildMap {
if (source != null) put("Source", source)
put(AnalyticsParam.TOKEN_PARAM, tokenSymbol)
put(AnalyticsParam.BLOCKCHAIN, blockchain)
if (source != null) put(AnalyticsParam.SOURCE, source)
},
)
fun addToNotMainAccount() = PortfolioAnalyticsEvent(
event = "Button - Add To Account",
category = category,
params = buildMap {
if (source != null) put("Source", source)
},
@ -37,6 +44,7 @@ internal class PortfolioAnalyticsEvent(
fun addButtonClick() = PortfolioAnalyticsEvent(
event = "Button - Add Token",
category = category,
params = buildMap {
if (source != null) put("Source", source)
},
@ -44,6 +52,7 @@ internal class PortfolioAnalyticsEvent(
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(
event = "Wallet Selected",
category = category,
params = buildMap {
if (source != null) put("Source", source)
},
@ -51,6 +60,7 @@ internal class PortfolioAnalyticsEvent(
fun addToPortfolioContinue(blockchainNames: List<String>) = PortfolioAnalyticsEvent(
event = "Token Network Selected",
category = category,
params = buildMap {
put("Count", blockchainNames.size.toString())
put("Token", tokenSymbol)
@ -61,9 +71,10 @@ internal class PortfolioAnalyticsEvent(
fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent(
event = "Token Added",
category = category,
params = buildMap {
put("Token", tokenSymbol)
put("Blockchain", blockchainName)
put(AnalyticsParam.TOKEN_PARAM, tokenSymbol)
put(AnalyticsParam.BLOCKCHAIN, blockchainName)
if (source != null) put("Source", source)
},
)
@ -76,6 +87,7 @@ internal class PortfolioAnalyticsEvent(
TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake"
else -> "error"
},
category = category,
params = buildMap {
if (source != null) put("Source", source)
},
@ -83,6 +95,7 @@ internal class PortfolioAnalyticsEvent(
fun getTokenLater() = PortfolioAnalyticsEvent(
event = "Popup Get token - Button Later",
category = category,
params = buildMap {
if (source != null) put("Source", source)
},

View file

@ -3,8 +3,8 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.converter
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
@ -27,7 +27,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
suspend fun convert(
balances: Map<UserWalletId, PortfolioFetcher.PortfolioBalance>,
availableNetworks: Set<TokenMarketInfo.Network>,
marketParams: TokenMarketParams,
marketParams: RawMarketToken,
): AvailableToAddData {
suspend fun AccountStatus.CryptoPortfolio.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? {
val currencies = availableNetworks
@ -103,7 +103,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
private suspend fun createCryptoCurrency(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
marketParams: TokenMarketParams,
marketParams: RawMarketToken,
account: Account.CryptoPortfolio,
): CryptoCurrency? {
return getTokenMarketCryptoCurrency(

View file

@ -2,8 +2,8 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.model
import arrow.core.getOrElse
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
@ -21,7 +21,7 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
availableToAddData: AvailableToAddData,
orderedNetworks: List<TokenMarketInfo.Network>,
selectedWallet: UserWallet?,
tokenParams: TokenMarketParams,
tokenParams: RawMarketToken,
accountToAdd: AvailableToAddAccount? = null,
): InitialSelection? {
if (availableToAddData.availableToAddWallets.isEmpty()) return null
@ -85,7 +85,7 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
userWallet: UserWallet,
account: AvailableToAddAccount,
orderedNetworks: List<TokenMarketInfo.Network>,
tokenParams: TokenMarketParams,
tokenParams: RawMarketToken,
): TokenMarketInfo.Network? {
val availableOrdered = orderedNetworks.filter { candidate ->
account.availableToAddNetworks.any { it.networkId == candidate.networkId }

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -26,7 +26,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.*
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
@ -78,8 +77,18 @@ internal class AddToPortfolioModel @Inject constructor(
val tokenActionsData: MutableSharedFlow<CryptoCurrencyData> = replayMutableSharedFlow()
val addToPortfolioManager: AddToPortfolioManager = params.addToPortfolioManager
val portfolioFetcher: PortfolioFetcher = addToPortfolioManager.portfolioFetcher
val eventBuilder: MutableSharedFlow<PortfolioAnalyticsEvent.EventBuilder> = replayMutableSharedFlow()
val paramsSnapshot: AddToPortfolioManager.Params by lazy {
addToPortfolioManager.paramsFlow.replayCache.first()
}
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder by lazy {
val tokenMarketParams = paramsSnapshot.token
PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = tokenMarketParams.symbol,
source = addToPortfolioManager.analyticsParams.source,
category = addToPortfolioManager.analyticsParams.category,
)
}
val userPortfolioStateController = userPortfolioStateControllerFactory.create(
modelScope = modelScope,
@ -120,12 +129,6 @@ internal class AddToPortfolioModel @Inject constructor(
.distinctUntilChanged()
.stateIn(this)
val isAccountMode = portfolioSelectorController.isAccountModeSync()
val tokenMarketParams = addToPortfolioManager.paramsFlow.first().token
val eb = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = tokenMarketParams.symbol,
source = addToPortfolioManager.analyticsParams.source,
)
eventBuilder.tryEmit(eb)
// use snapshot data, looks like we dont need to remap at runtime
val data = featureDataFlow.value
@ -172,11 +175,12 @@ internal class AddToPortfolioModel @Inject constructor(
)
// suspend until all required data is selected
allRequireForAdd.first()
val firstPair = allRequireForAdd.first()
// line of navigation to AddToken screen is finished; cancel the job, select a new root screen
firstPartOfNavigation.cancel()
analyticsEventHandler.send(event = eventBuilder.first().popupToConfirm())
val selectedNetworkName = firstPair.first.cryptoCurrency.network.name
analyticsEventHandler.send(event = eventBuilder.popupToConfirm(selectedNetworkName))
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
var middleNavigationJob: Job? = null
@ -204,6 +208,10 @@ internal class AddToPortfolioModel @Inject constructor(
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,
@ -225,6 +233,7 @@ internal class AddToPortfolioModel @Inject constructor(
}
callbackDelegate.onLaterClick.receiveAsFlow().first()
analyticsEventHandler.send(eventBuilder.getTokenLater())
finishSuccessFlow(result)
}
.catch { throwable ->
@ -247,29 +256,22 @@ internal class AddToPortfolioModel @Inject constructor(
channel.close()
}
val tokenMarketParams = addToPortfolioManager.paramsFlow.first().token
val eb = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = tokenMarketParams.symbol,
source = addToPortfolioManager.analyticsParams.source,
)
eventBuilder.tryEmit(eb)
val tokenMarketParams = paramsSnapshot.token
val launchMode = addToPortfolioManager.settings.launchMode
val launchMode = paramsSnapshot.launchMode
val initialData = featureData
.filterIsInstance<AddToPortfolioManager.State.Ready>()
.map { it.availableToAddData }
.first()
if (launchMode is AddToPortfolioManager.LaunchMode.ViaUserPortfolio &&
initialData.hasAnyAddedCurrency(launchMode.rawCurrencyId)
initialData.hasAnyAddedCurrency(tokenMarketParams.id)
) {
// suspend, must prepare UM before navigate to UserPortfolio
userPortfolioStateController.updateAndWaitNotNullState(initialData, launchMode.rawCurrencyId)
userPortfolioStateController.updateAndWaitNotNullState(initialData, tokenMarketParams.id)
navigation.replaceAll(AddToPortfolioRoutes.UserPortfolio)
callbackDelegate.onContinueFromUserPortfolio.receiveAsFlow().first()
}
val paramsSnapshot = addToPortfolioManager.paramsFlow.first()
val selection = selectionResolver.resolve(
availableToAddData = initialData,
orderedNetworks = paramsSnapshot.networks,
@ -297,7 +299,8 @@ internal class AddToPortfolioModel @Inject constructor(
selectedPortfolio.emit(firstSelectedPortfolio)
selectedNetwork.emit(firstSelectedNetwork)
analyticsEventHandler.send(event = eventBuilder.first().popupToConfirm())
val selectedNetworkName = firstSelectedNetwork.cryptoCurrency.network.name
analyticsEventHandler.send(event = eventBuilder.popupToConfirm(selectedNetworkName))
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
var middleNavigationJob: Job? = null
@ -349,6 +352,7 @@ internal class AddToPortfolioModel @Inject constructor(
.launchIn(this)
callbackDelegate.onLaterClick.receiveAsFlow().first()
analyticsEventHandler.send(eventBuilder.getTokenLater())
finishSuccessFlow(result)
}
.catch { throwable ->
@ -358,9 +362,9 @@ internal class AddToPortfolioModel @Inject constructor(
.launchIn(modelScope)
}
private suspend fun logAccountSelector(isAccountMode: Boolean) {
private fun logAccountSelector(isAccountMode: Boolean) {
if (isAccountMode) {
analyticsEventHandler.send(eventBuilder.first().popupToChooseAccount())
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
}
}
@ -402,7 +406,7 @@ internal class AddToPortfolioModel @Inject constructor(
private fun changePortfolioNavigationNewFlow(
data: AvailableToAddData,
orderedNetworks: List<TokenMarketInfo.Network>,
tokenParams: TokenMarketParams,
tokenParams: RawMarketToken,
): Flow<Unit> {
return setupPortfolioFlow(data)
// drop first selected portfolio or any selected before
@ -473,7 +477,7 @@ internal class AddToPortfolioModel @Inject constructor(
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
val availableToAddAccount =
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.first().addToPortfolioWalletChanged())
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,

View file

@ -12,8 +12,8 @@ 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.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
@ -31,7 +31,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.collections.mapNotNull
@ -66,6 +65,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = params.tokenToAdd.symbol,
source = AnalyticsParam.ScreensSources.Markets.value,
category = AnalyticsParam.ScreensSources.Markets.value,
)
init {
@ -149,17 +149,10 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
}
private fun minimalTokenMarketParams() = with(params.tokenToAdd) {
TokenMarketParams(
RawMarketToken(
id = id,
name = name,
symbol = symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = BigDecimal.ZERO,
h24Percent = null,
weekPercent = null,
monthPercent = null,
),
imageUrl = null,
)
}

View file

@ -85,7 +85,7 @@ internal class AddTokenModel @Inject constructor(
uiState.value = um.toggleProgress(true)
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
val analyticsEventBuilder = params.eventBuilder.first()
val analyticsEventBuilder = params.eventBuilder
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
analyticsEventHandler.send(analyticsEventBuilder.addButtonClick())
@ -105,14 +105,6 @@ internal class AddTokenModel @Inject constructor(
if (status == null) {
processError(error = null)
} else {
if (!account.isMainAccount) {
analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount())
}
analyticsEventHandler.send(
event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name),
)
params.callbacks.onTokenAdded(status.status)
}
uiState.value = um.toggleProgress(false)

View file

@ -64,7 +64,6 @@ internal class TokenActionsModel @Inject constructor(
uiBuilder.build(
cryptoCurrencyData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
eventBuilder = analyticsEventBuilder.first(),
appCurrency = currentAppCurrency.value,
isBalanceHidden = isBalanceHidden,
)
@ -76,7 +75,7 @@ internal class TokenActionsModel @Inject constructor(
)
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction) = modelScope.launch {
val event = analyticsEventBuilder.first().getTokenActionClick(actionUM = handledAction.action)
val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action)
analyticsEventHandler.send(event)
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
if (!isReceive) return@launch

View file

@ -8,7 +8,6 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
import com.tangem.common.ui.markets.action.CryptoCurrencyData
import com.tangem.common.ui.markets.action.QuickActionsConverter.quickActions
import com.tangem.common.ui.markets.action.TokenActionsHandler
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.DesignFeatureToggles
@ -25,7 +24,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import java.math.BigDecimal
import javax.inject.Inject
@ -33,7 +31,6 @@ import javax.inject.Inject
@ModelScoped
internal class TokenActionsUiBuilder @Inject constructor(
paramsContainer: ParamsContainer,
private val analyticsEventHandler: AnalyticsEventHandler,
private val designFeatureToggles: DesignFeatureToggles,
) {
private val params = paramsContainer.require<TokenActionsComponent.Params>()
@ -41,7 +38,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
fun build(
cryptoCurrencyData: CryptoCurrencyData,
tokenActionsHandler: TokenActionsHandler,
eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): TokenActionsUM {
@ -49,7 +45,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
buildV2(
cryptoCurrencyData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
eventBuilder = eventBuilder,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
)
@ -57,7 +52,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
buildV1(
cryptoCurrencyData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
eventBuilder = eventBuilder,
)
}
}
@ -65,7 +59,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
private fun buildV1(
cryptoCurrencyData: CryptoCurrencyData,
tokenActionsHandler: TokenActionsHandler,
eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
): TokenActionsUM {
val status = cryptoCurrencyData.status
val tokenUM = TokenItemState.Content(
@ -86,7 +79,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
isRedesignEnabled = false,
),
onLaterClick = {
analyticsEventHandler.send(eventBuilder.getTokenLater())
params.callbacks.onLaterClick()
},
)
@ -95,7 +87,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
private fun buildV2(
cryptoCurrencyData: CryptoCurrencyData,
tokenActionsHandler: TokenActionsHandler,
eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): TokenActionsUM {
@ -118,7 +109,6 @@ internal class TokenActionsUiBuilder @Inject constructor(
isRedesignEnabled = true,
),
onLaterClick = {
analyticsEventHandler.send(eventBuilder.getTokenLater())
params.callbacks.onLaterClick()
},
isBalancesHidden = isBalanceHidden,

View file

@ -1,11 +1,10 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.ui
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.Settings
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.*
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
import com.tangem.features.commonfeatures.impl.addtoportfolio.converter.AvailableToAddDataConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
@ -25,8 +24,8 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
) : AddToPortfolioManager {
override val onDismiss: Channel<Unit> = Channel()
override val onSuccessAdded: Channel<AddToPortfolioManager.Result> = Channel()
override val onAddedTokenClick: Channel<AddToPortfolioManager.Result> = Channel()
override val onSuccessAdded: Channel<Result> = Channel()
override val onAddedTokenClick: Channel<Result> = Channel()
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
@ -37,17 +36,17 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
override val paramsFlow = internalParamsFlow
.transform { internalParams ->
val fullParams = AddToPortfolioManager.Params(
val fullParams = Params(
networks = internalParams.networks ?: return@transform,
token = internalParams.token ?: return@transform,
launchMode = internalParams.launchMode,
)
emit(fullParams)
}
.distinctUntilChanged()
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
override val state: MutableStateFlow<AddToPortfolioManager.State> =
MutableStateFlow(AddToPortfolioManager.State.Loading)
override val state: MutableStateFlow<State> = MutableStateFlow(State.Loading)
init {
buildFlow()
@ -60,11 +59,11 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
onDismiss.trySend(Unit)
}
override fun onSuccessAdded(result: AddToPortfolioManager.Result) {
override fun onSuccessAdded(result: Result) {
onSuccessAdded.trySend(result)
}
override fun onAddedTokenClick(result: AddToPortfolioManager.Result) {
override fun onAddedTokenClick(result: Result) {
onAddedTokenClick.trySend(result)
}
@ -72,23 +71,32 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
updateInternal(networks = networks)
}
override fun setTokenParams(token: TokenMarketParams) {
override fun setTokenParams(token: RawMarketToken) {
updateInternal(token = token)
}
private fun updateInternal(networks: List<TokenMarketInfo.Network>? = null, token: TokenMarketParams? = null) {
override fun updateLaunchMode(launchMode: LaunchMode) {
updateInternal(launchMode = launchMode)
}
private fun updateInternal(
networks: List<TokenMarketInfo.Network>? = null,
token: RawMarketToken? = null,
launchMode: LaunchMode? = null,
) {
internalParamsFlow.update { prev ->
val newParams = ParamsInternal(
networks = networks ?: prev.networks,
token = token ?: prev.token,
launchMode = launchMode ?: prev.launchMode,
)
val shouldReload = prev.networks != newParams.networks || prev.token != newParams.token
if (shouldReload) state.update { AddToPortfolioManager.State.Loading }
val shouldReload = newParams != prev
if (shouldReload) state.update { State.Loading }
return@update newParams
}
}
private fun buildFlow(): Flow<AddToPortfolioManager.State> = combine(
private fun buildFlow(): Flow<State> = combine(
flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(),
flow2 = paramsFlow,
) { balances, (availableNetworks, token) ->
@ -97,7 +105,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
availableNetworks = availableNetworks.toSet(),
marketParams = token,
)
AddToPortfolioManager.State.Ready(data)
State.Ready(data)
}
@AssistedFactory
@ -110,7 +118,8 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
}
private data class ParamsInternal(
val launchMode: LaunchMode = LaunchMode.DirectAdd,
val networks: List<TokenMarketInfo.Network>? = null,
val token: TokenMarketParams? = null,
val token: RawMarketToken? = null,
)
}

View file

@ -4,8 +4,8 @@ import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.RawMarketToken
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
@ -31,7 +31,7 @@ class AddToPortfolioInitialSelectionResolverTest {
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency = mockk()
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase = mockk()
private val tokenParams: TokenMarketParams = mockk()
private val tokenParams: RawMarketToken = mockk()
private lateinit var resolver: AddToPortfolioInitialSelectionResolver

View file

@ -99,7 +99,7 @@ internal class DefaultMarketsTokenDetailsComponent(
}
override fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
model.openAddToPortfolioViaUserPortfolio(rawCurrencyId)
model.openAddToPortfolioViaUserPortfolio()
}
},
)
@ -149,10 +149,9 @@ internal class DefaultMarketsTokenDetailsComponent(
@Suppress("UNUSED_PARAMETER") config: AddToPortfolioSlotRoute,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val manager = model.addToPortfolioManagerOrNull() ?: return ComposableBottomSheetComponent.EMPTY
return addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(addToPortfolioManager = manager),
params = AddToPortfolioComponent.Params(addToPortfolioManager = model.addToPortfolioManager),
)
}

View file

@ -40,7 +40,6 @@ import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsUseCase
@ -72,7 +71,6 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -238,8 +236,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private val isAddToPortfolioAvailable: Boolean =
params.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled
private var addToPortfolioManager: AddToPortfolioManager? = null
private var addToPortfolioListenersJob: Job? = null
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings(
shouldSkipTokenActionsScreen = false,
),
analyticsParams = AddToPortfolioManager.AnalyticsParams(params.analyticsParams?.source),
)
val state = MutableStateFlow(
MarketsTokenDetailsUM(
@ -329,7 +332,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
modelScope.launch {
networksState.collect { tokenNetworkState ->
val manager = addToPortfolioManager ?: return@collect
val manager = addToPortfolioManager
when (tokenNetworkState) {
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(tokenNetworkState.networks)
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
@ -337,6 +340,18 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
}
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { addToPortfolioSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { addToPortfolioSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onAddedTokenClick.receiveAsFlow()
.onEach { result ->
addToPortfolioSheetNavigation.dismiss()
openTokenDetails(result)
}
.launchIn(modelScope)
}
fun openAddToPortfolio() {
@ -345,9 +360,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
}
fun openAddToPortfolioViaUserPortfolio(rawCurrencyId: CryptoCurrency.RawID) {
fun openAddToPortfolioViaUserPortfolio() {
if (!isAddToPortfolioAvailable) return
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.ViaUserPortfolio(rawCurrencyId))
prepareAddToPortfolioManager(AddToPortfolioManager.LaunchMode.ViaUserPortfolio)
addToPortfolioSheetNavigation.activate(AddToPortfolioSlotRoute)
}
@ -360,43 +375,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
}
fun addToPortfolioManagerOrNull(): AddToPortfolioManager? = addToPortfolioManager
private fun prepareAddToPortfolioManager(launchMode: AddToPortfolioManager.LaunchMode) {
addToPortfolioListenersJob?.cancel()
val manager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings(
shouldSkipTokenActionsScreen = false,
launchMode = launchMode,
),
analyticsParams = AddToPortfolioManager.AnalyticsParams(params.analyticsParams?.source),
)
val manager = addToPortfolioManager
manager.setTokenParams(params.token)
manager.updateLaunchMode(launchMode)
when (val network = networksState.value) {
is TokenNetworksState.NetworksAvailable -> manager.setTokenNetworks(network.networks)
TokenNetworksState.NoNetworksAvailable -> manager.setTokenNetworks(emptyList())
else -> Unit
}
addToPortfolioManager = manager
addToPortfolioListenersJob = modelScope.launch {
launch {
manager.onDismiss.receiveAsFlow().collect {
addToPortfolioSheetNavigation.dismiss()
}
}
launch {
manager.onSuccessAdded.receiveAsFlow().collect {
addToPortfolioSheetNavigation.dismiss()
}
}
launch {
manager.onAddedTokenClick.receiveAsFlow().collect { result ->
addToPortfolioSheetNavigation.dismiss()
openTokenDetails(result)
}
}
}
}
private fun initialLoad() {