Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 15:45:21 +04:00
parent 5f619a56fe
commit 9a0fbcbe1b
22 changed files with 324 additions and 254 deletions

View file

@ -127,24 +127,6 @@ class AccountCryptoPortfolioItemStateConverter(
isFlickering = this.source.isFlickering(),
)
private fun createFiatAmountState(fiatBalance: TotalFiatBalance, appCurrency: AppCurrency): FiatAmountState {
return when (fiatBalance) {
TotalFiatBalance.Failed,
TotalFiatBalance.Loading,
-> FiatAmountState.Empty
is TotalFiatBalance.Loaded -> FiatAmountState.Content(
text = fiatBalance.amount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
isFlickering = fiatBalance.source == StatusSource.CACHE,
)
}
}
private fun createSubtitle2State(priceChangeLce: Lce<Unit, PriceChange>?): Subtitle2State? {
return priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
@ -152,4 +134,24 @@ class AccountCryptoPortfolioItemStateConverter(
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
companion object {
fun createFiatAmountState(fiatBalance: TotalFiatBalance, appCurrency: AppCurrency): FiatAmountState {
return when (fiatBalance) {
TotalFiatBalance.Failed,
TotalFiatBalance.Loading,
-> FiatAmountState.Empty
is TotalFiatBalance.Loaded -> FiatAmountState.Content(
text = fiatBalance.amount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
isFlickering = fiatBalance.source == StatusSource.CACHE,
)
}
}
}
}

View file

@ -2,21 +2,12 @@ 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.models.currency.CryptoCurrency
interface AddToPortfolioComponent : ComposableBottomSheetComponent {
data class Params(
val addToPortfolioManager: AddToPortfolioManager,
val callback: Callback,
val shouldSkipTokenActionsScreen: Boolean = false,
)
interface Callback {
fun onDismiss()
// todo swap add new onSuccess with full data of added token
fun onSuccess(addedToken: CryptoCurrency)
}
interface Factory : ComponentFactory<Params, AddToPortfolioComponent>
}

View file

@ -2,43 +2,90 @@ package com.tangem.features.commonfeatures.api.addtoportfolio
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.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
interface AddToPortfolioManager {
interface AddToPortfolioManager : AddToPortfolioManagerInternal {
val onDismiss: Channel<Unit>
val onSuccessAdded: Channel<Result>
// todo swap make updatable
val token: TokenMarketParams
val analyticsParams: AnalyticsParams?
val portfolioFetcher: PortfolioFetcher
val state: StateFlow<State>
val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>>
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
fun setTokenParams(token: TokenMarketParams)
sealed interface State {
data object Init : State
data class AvailableToAdd(
data object Loading : State
data class Ready(
val availableToAddData: AvailableToAddData,
) : State
data object NothingToAdd : State
) : State {
val isAvailableToAdd: Boolean get() = availableToAddData.isAvailableToAdd
val isSinglePortfolio: Boolean get() = availableToAddData.isSinglePortfolio
}
}
@Serializable
data class AnalyticsParams(
val source: String,
val source: String?,
)
interface Factory {
fun create(
scope: CoroutineScope,
token: TokenMarketParams,
analyticsParams: AnalyticsParams?,
): AddToPortfolioManager
fun create(scope: CoroutineScope, settings: Settings, analyticsParams: AnalyticsParams): AddToPortfolioManager
}
/**
* Immutable settings
*/
data class Settings(
val shouldSkipTokenActionsScreen: Boolean = false,
) {
companion object {
val DefaultMarket = Settings(
shouldSkipTokenActionsScreen = false,
)
val ChooseToken = Settings(
shouldSkipTokenActionsScreen = true,
)
}
}
/**
* Mutable parameters
* Updates may trigger reload [State]
*/
data class Params(
val networks: List<TokenMarketInfo.Network>,
val token: TokenMarketParams,
)
data class Result(
val wallet: UserWallet,
val account: AccountStatus.CryptoPortfolio,
val addedCurrency: CryptoCurrencyStatus,
)
}
/**
* primary for internal impl usage, but you can also use it externally
*/
interface AddToPortfolioManagerInternal {
val paramsFlow: SharedFlow<AddToPortfolioManager.Params>
val settings: AddToPortfolioManager.Settings
val analyticsParams: AnalyticsParams
suspend fun token(): TokenMarketParams = paramsFlow.first().token
fun onDismiss()
fun onSuccessAdded(result: AddToPortfolioManager.Result)
}

View file

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

View file

@ -71,7 +71,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
}
override fun dismiss() {
params.callback.onDismiss()
model.addToPortfolioManager.onDismiss()
}
@Composable

View file

@ -10,12 +10,13 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioPreselectedDataModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent
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,
@ -37,7 +38,7 @@ internal class DefaultAddToPortfolioPreselectedDataComponent @AssistedInject con
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
context = child("addTokenComponent"),
params = AddTokenComponent.Params(
eventBuilder = model.eventBuilder,
eventBuilder = flowOf(model.eventBuilder),
callbacks = model,
selectedPortfolio = model.selectedPortfolio,
selectedNetwork = model.selectedNetwork,

View file

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

View file

@ -19,6 +19,7 @@ import com.tangem.domain.markets.TokenMarketInfo
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.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.commonfeatures.api.addtoportfolio.*
import com.tangem.features.commonfeatures.impl.R
@ -63,12 +64,9 @@ internal class AddToPortfolioModel @Inject constructor(
val selectedPortfolio: MutableSharedFlow<SelectedPortfolio> = replayMutableSharedFlow()
val tokenActionsData: MutableSharedFlow<CryptoCurrencyData> = replayMutableSharedFlow()
private val addToPortfolioManager = params.addToPortfolioManager
val portfolioFetcher = addToPortfolioManager.portfolioFetcher
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
tokenSymbol = addToPortfolioManager.token.symbol,
source = addToPortfolioManager.analyticsParams?.source,
)
val addToPortfolioManager: AddToPortfolioManager = params.addToPortfolioManager
val portfolioFetcher: PortfolioFetcher = addToPortfolioManager.portfolioFetcher
val eventBuilder: MutableSharedFlow<PortfolioAnalyticsEvent.EventBuilder> = replayMutableSharedFlow()
val featureData: Flow<AddToPortfolioManager.State> = combineFeatureData()
@ -85,32 +83,27 @@ internal class AddToPortfolioModel @Inject constructor(
@Suppress("LongMethod")
private fun startAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishFlow() {
params.callback.onDismiss()
channel.close()
}
fun finishSuccessFlow(addedToken: CryptoCurrencyStatus) {
params.callback.onSuccess(addedToken.currency)
fun finishSuccessFlow(result: AddToPortfolioManager.Result) {
addToPortfolioManager.onSuccessAdded(result)
channel.close()
}
val featureDataFlow: StateFlow<AvailableToAddData> = featureData
.filterIsInstance<AddToPortfolioManager.State.AvailableToAdd>()
.filterIsInstance<AddToPortfolioManager.State.Ready>()
.map { it.availableToAddData }
.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
// you must control it via [AddToPortfolioManager.state]
if (!data.isAvailableToAdd) {
finishFlow()
return@channelFlow
}
// init data flows, emits on user/code selection, updates state holder
val firstSelectedPortfolio = setupPortfolioFlow(data)
.onEach { selectedPortfolio.emit(it) }
@ -157,7 +150,7 @@ internal class AddToPortfolioModel @Inject constructor(
// line of navigation to AddToken screen is finished; cancel the job, select a new root screen
firstPartOfNavigation.cancel()
analyticsEventHandler.send(event = eventBuilder.popupToConfirm())
analyticsEventHandler.send(event = eventBuilder.first().popupToConfirm())
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
var middleNavigationJob: Job? = null
@ -185,34 +178,39 @@ internal class AddToPortfolioModel @Inject constructor(
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
middleNavigationJob?.cancel()
val selectedPortfolio = selectedPortfolio.first()
val result = AddToPortfolioManager.Result(
wallet = selectedPortfolio.userWallet,
account = selectedPortfolio.account.account,
addedCurrency = addedToken,
)
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
if (params.shouldSkipTokenActionsScreen) {
finishSuccessFlow(addedToken)
if (addToPortfolioManager.settings.shouldSkipTokenActionsScreen) {
finishSuccessFlow(result)
} else {
setupTokenActionsFlow(selectedPortfolio, addedToken)
.onEach { cryptoCurrencyData ->
tokenActionsData.emit(cryptoCurrencyData)
navigation.replaceAll(AddToPortfolioRoutes.TokenActions)
}
.onEmpty { finishFlow() }
.onEmpty { finishSuccessFlow(result) }
.launchIn(this)
}
callbackDelegate.onLaterClick.receiveAsFlow().first()
finishFlow()
finishSuccessFlow(result)
}
.catch { throwable ->
TangemLogger.e("Error", throwable)
params.callback.onDismiss()
addToPortfolioManager.onDismiss()
}
.launchIn(modelScope)
}
private fun logAccountSelector(isAccountMode: Boolean) {
private suspend fun logAccountSelector(isAccountMode: Boolean) {
if (isAccountMode) {
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
analyticsEventHandler.send(eventBuilder.first().popupToChooseAccount())
}
}
@ -291,7 +289,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.addToPortfolioWalletChanged())
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.first().addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,
@ -327,7 +325,7 @@ internal class AddToPortfolioModel @Inject constructor(
val accountIndex = account.account.account.derivationIndex
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = addToPortfolioManager.token,
tokenMarketParams = addToPortfolioManager.token(),
network = network,
accountIndex = accountIndex,
)
@ -339,7 +337,7 @@ internal class AddToPortfolioModel @Inject constructor(
private fun combineFeatureData() = addToPortfolioManager.state.onEach { state ->
when (state) {
is AddToPortfolioManager.State.AvailableToAdd ->
is AddToPortfolioManager.State.Ready ->
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
?: return@isEnabled false
@ -348,8 +346,7 @@ internal class AddToPortfolioModel @Inject constructor(
?.isAvailableToAdd == true
return@isEnabled isAvailableAccount
}
AddToPortfolioManager.State.Init,
AddToPortfolioManager.State.NothingToAdd,
AddToPortfolioManager.State.Loading,
-> Unit
}
}

View file

@ -12,11 +12,11 @@ import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress
import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedNetwork
import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -40,7 +40,6 @@ internal class AddTokenModel @Inject constructor(
) : Model() {
private val params = paramsContainer.require<AddTokenComponent.Params>()
private val analyticsEventBuilder = params.eventBuilder
private val addTokenJob = JobHolder()
val uiState: StateFlow<AddTokenUM?>
@ -86,10 +85,11 @@ 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()
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
analyticsEventHandler.send(analyticsEventBuilder.addButtonClick())
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
manageCryptoCurrenciesUseCase.invokeAndAwait(accountId = accountId, add = listOf(cryptoCurrency))
.onLeft { throwable ->
processError(error = throwable)
uiState.value = um.toggleProgress(false)
@ -100,7 +100,8 @@ internal class AddTokenModel @Inject constructor(
userWalletId = accountId.userWalletId,
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
).firstOrNull()
)
.firstOrNull()
if (status == null) {
processError(error = null)
} else {

View file

@ -18,6 +18,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -52,18 +53,18 @@ internal class TokenActionsModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
val uiState: StateFlow<TokenActionsUM?> = params.data
.mapLatest { uiBuilder.build(it, tokenActionsHandler) }
.mapLatest { uiBuilder.build(it, tokenActionsHandler, analyticsEventBuilder.first()) }
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = null,
)
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction) {
val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action)
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction) = modelScope.launch {
val event = analyticsEventBuilder.first().getTokenActionClick(actionUM = handledAction.action)
analyticsEventHandler.send(event)
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
if (!isReceive) return
if (!isReceive) return@launch
modelScope.launch {
val tokenConfig = receiveAddressesFactory.create(
status = handledAction.cryptoCurrencyData.status,

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
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 javax.inject.Inject
@ -20,7 +21,11 @@ internal class TokenActionsUiBuilder @Inject constructor(
) {
private val params = paramsContainer.require<TokenActionsComponent.Params>()
fun build(data: CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM {
fun build(
data: CryptoCurrencyData,
tokenActionsHandler: TokenActionsHandler,
eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
): TokenActionsUM {
val status = data.status
val tokenUM = TokenItemState.Content(
id = status.currency.id.value,
@ -35,7 +40,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
return TokenActionsUM(
token = tokenUM,
onLaterClick = {
analyticsEventHandler.send(params.eventBuilder.getTokenLater())
analyticsEventHandler.send(eventBuilder.getTokenLater())
params.callbacks.onLaterClick()
},
quickActions = quickActions(data, tokenActionsHandler),

View file

@ -3,69 +3,109 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.ui
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.commonfeatures.impl.addtoportfolio.converter.AvailableToAddDataConverter
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.impl.addtoportfolio.converter.AvailableToAddDataConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
internal class DefaultAddToPortfolioManager @AssistedInject constructor(
private val availableToAddDataConverter: AvailableToAddDataConverter,
@Assisted override val token: TokenMarketParams,
@Assisted override val analyticsParams: AddToPortfolioManager.AnalyticsParams?,
@Assisted override val settings: Settings,
@Assisted override val analyticsParams: AnalyticsParams,
@Assisted val scope: CoroutineScope,
dispatchers: CoroutineDispatcherProvider,
portfolioFetcherFactory: PortfolioFetcher.Factory,
) : AddToPortfolioManager {
private val _allAvailableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val onDismiss: Channel<Unit> = Channel()
override val onSuccessAdded: Channel<AddToPortfolioManager.Result> = Channel()
override val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>> = _allAvailableNetworks.asSharedFlow()
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = scope,
)
override val state: StateFlow<AddToPortfolioManager.State> =
combine(
flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(),
flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(),
) { balances, availableNetworks ->
val data = availableToAddDataConverter.convert(
balances = balances,
availableNetworks = availableNetworks,
marketParams = token,
private val internalParamsFlow = MutableStateFlow(ParamsInternal())
override val paramsFlow = internalParamsFlow
.transform { internalParams ->
val fullParams = AddToPortfolioManager.Params(
networks = internalParams.networks ?: return@transform,
token = internalParams.token ?: return@transform,
)
if (data.isAvailableToAdd) {
AddToPortfolioManager.State.AvailableToAdd(data)
} else {
AddToPortfolioManager.State.NothingToAdd
}
emit(fullParams)
}
.distinctUntilChanged()
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
override val state: MutableStateFlow<AddToPortfolioManager.State> =
MutableStateFlow(AddToPortfolioManager.State.Loading)
init {
buildFlow()
.onEach { newState -> state.update { newState } }
.flowOn(dispatchers.default)
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = AddToPortfolioManager.State.Init,
)
.launchIn(scope)
}
override fun onDismiss() {
onDismiss.trySend(Unit)
}
override fun onSuccessAdded(result: AddToPortfolioManager.Result) {
onSuccessAdded.trySend(result)
}
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
_allAvailableNetworks.tryEmit(networks)
updateInternal(networks = networks)
}
override fun setTokenParams(token: TokenMarketParams) {
updateInternal(token = token)
}
private fun updateInternal(networks: List<TokenMarketInfo.Network>? = null, token: TokenMarketParams? = null) {
internalParamsFlow.update { prev ->
val newParams = ParamsInternal(
networks = networks ?: prev.networks,
token = token ?: prev.token,
)
val shouldReload = prev.networks != newParams.networks || prev.token != newParams.token
if (shouldReload) state.update { AddToPortfolioManager.State.Loading }
return@update newParams
}
}
private fun buildFlow(): Flow<AddToPortfolioManager.State> = combine(
flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(),
flow2 = paramsFlow,
) { balances, (availableNetworks, token) ->
val data = availableToAddDataConverter.convert(
balances = balances,
availableNetworks = availableNetworks.toSet(),
marketParams = token,
)
AddToPortfolioManager.State.Ready(data)
}
@AssistedFactory
interface Factory : AddToPortfolioManager.Factory {
override fun create(
scope: CoroutineScope,
token: TokenMarketParams,
analyticsParams: AddToPortfolioManager.AnalyticsParams?,
settings: Settings,
analyticsParams: AnalyticsParams,
): DefaultAddToPortfolioManager
}
private data class ParamsInternal(
val networks: List<TokenMarketInfo.Network>? = null,
val token: TokenMarketParams? = null,
)
}

View file

@ -66,7 +66,6 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = model.addToPortfolioManager,
callback = model.addToPortfolioCallback,
),
)
is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create(

View file

@ -14,9 +14,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
@ -58,16 +56,18 @@ internal class MarketsPortfolioModel @Inject constructor(
private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate()
val bottomSheetNavigation: SlotNavigation<MarketsPortfolioRoute> = SlotNavigation()
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
override fun onDismiss() = bottomSheetNavigation.dismiss()
override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss()
}
init {
marketsPortfolioDelegate.combineData()
.onEach { state.value = it }
.flowOn(dispatchers.default)
.launchIn(modelScope)
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
}
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
@ -83,9 +83,9 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun createAddToPortfolioManager(): AddToPortfolioManager {
return addToPortfolioManagerFactory.create(
scope = modelScope,
token = params.token,
analyticsParams = params.analyticsParams?.source?.let { AddToPortfolioManager.AnalyticsParams(it) },
)
settings = AddToPortfolioManager.Settings.DefaultMarket,
analyticsParams = AddToPortfolioManager.AnalyticsParams(params.analyticsParams?.source),
).also { addToPortfolioManager -> addToPortfolioManager.setTokenParams(params.token) }
}
private fun createMarketsPortfolioDelegate(): MarketsPortfolioDelegate {
@ -95,11 +95,14 @@ internal class MarketsPortfolioModel @Inject constructor(
tokenActionsHandler = tokenActionsHandler,
buttonState = addToPortfolioManager.state.map { state ->
when (state) {
is AddToPortfolioManager.State.AvailableToAdd -> {
MyPortfolioUM.Tokens.AddButtonState.Available
is AddToPortfolioManager.State.Ready -> {
if (state.isAvailableToAdd) {
MyPortfolioUM.Tokens.AddButtonState.Available
} else {
MyPortfolioUM.Tokens.AddButtonState.Unavailable
}
}
AddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading
AddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable
AddToPortfolioManager.State.Loading -> MyPortfolioUM.Tokens.AddButtonState.Loading
}
},
onAddClick = {

View file

@ -74,9 +74,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
return addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager!!,
callback = selectToTokenListComponent.addToPortfolioCallback,
shouldSkipTokenActionsScreen = true,
addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager,
),
)
}

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.decompose.ComposableListContentComponent
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
@ -18,8 +17,7 @@ import kotlinx.coroutines.flow.StateFlow
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute>
val addToPortfolioManager: AddToPortfolioManager?
val addToPortfolioCallback: AddToPortfolioComponent.Callback
val addToPortfolioManager: AddToPortfolioManager
/** Component factory */
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>

View file

@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
@ -26,8 +25,7 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
override val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> get() = model.bottomSheetNavigation
override val addToPortfolioManager: AddToPortfolioManager? get() = model.addToPortfolioManager
override val addToPortfolioCallback: AddToPortfolioComponent.Callback get() = model.addToPortfolioCallback
override val addToPortfolioManager: AddToPortfolioManager get() = model.addToPortfolioManager
override val uiState: StateFlow<TokenListUM>
get() = model.state

View file

@ -41,7 +41,6 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
@ -66,9 +65,7 @@ import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -104,14 +101,12 @@ internal class AvailableSwapPairsModel @Inject constructor(
private val allUserWallets = getWalletsUseCase.invokeSync()
val bottomSheetNavigation: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
var addToPortfolioManager: AddToPortfolioManager? = null
val addToPortfolioCallback: AddToPortfolioComponent.Callback = object : AddToPortfolioComponent.Callback {
override fun onDismiss() = bottomSheetNavigation.dismiss()
override fun onSuccess(addedToken: CryptoCurrency) {
onTokenAddedToPortfolio(addedToken)
}
}
private val addToPortfolioJobHolder = JobHolder()
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory
.create(
scope = modelScope,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
settings = AddToPortfolioManager.Settings.ChooseToken,
)
private val accountListFlow = getAccountListUseCaseFlow()
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
@ -159,6 +154,12 @@ internal class AvailableSwapPairsModel @Inject constructor(
subscribeOnMarketsUpdates()
subscribeOnVisibleMarketItems()
}
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { result -> onTokenAddedToPortfolio(result.addedCurrency.currency) }
.launchIn(modelScope)
}
private fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
@ -593,45 +594,35 @@ internal class AvailableSwapPairsModel @Inject constructor(
}
private fun addToPortfolioItem(item: MarketsListItemUM) {
modelScope.launch {
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
?: searchMarketsListManager.getTokenMarketById(item.id)
?: return@launch
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
?: searchMarketsListManager.getTokenMarketById(item.id)
?: return
val param = tokenMarket.toSerializableParam()
val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot }
val param = tokenMarket.toSerializableParam()
val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot }
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,
hotExcludedBlockchains = hotWalletExcludedBlockchains,
hasOnlyHotWallets = hasOnlyHotWallets,
)
}?.map { network ->
TokenMarketInfo.Network(
networkId = network.networkId,
isExchangeable = false,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
}.orEmpty()
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,
hotExcludedBlockchains = hotWalletExcludedBlockchains,
hasOnlyHotWallets = hasOnlyHotWallets,
)
}?.map { network ->
TokenMarketInfo.Network(
networkId = network.networkId,
isExchangeable = false,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
}.orEmpty()
addToPortfolioManager = addToPortfolioManagerFactory
.create(
scope = modelScope,
token = param,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
).apply {
setTokenNetworks(networks)
}
addToPortfolioManager.setTokenNetworks(networks)
addToPortfolioManager.setTokenParams(param)
addToPortfolioManager?.state
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) }
}.saveIn(addToPortfolioJobHolder)
bottomSheetNavigation.activate(AddToPortfolioRoute)
}
private fun subscribeOnVisibleMarketItems() {

View file

@ -54,9 +54,7 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor(
return addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = model.addToPortfolioManager!!,
callback = model.addToPortfolioCallback,
shouldSkipTokenActionsScreen = true,
addToPortfolioManager = model.addToPortfolioManager,
),
)
}

View file

@ -80,10 +80,13 @@ internal class ChooseTokenListItemConverter(
clickIntents.onAccountExpandClick(clickedAccount)
}
}
val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?) = if (isSearchingState) {
{ _ -> FiatAmountState.Empty }
} else {
{ _ -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative) }
val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?) = { totalBalance ->
when {
isSearchingState -> FiatAmountState.Empty
!isExpanded -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative)
else -> AccountCryptoPortfolioItemStateConverter
.createFiatAmountState(totalBalance, appCurrency)
}
}
val converter = AccountCryptoPortfolioItemStateConverter(

View file

@ -10,7 +10,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -23,7 +22,6 @@ import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@ -73,17 +71,6 @@ internal class ChooseTokenModel @Inject constructor(
private val expandedAccountsFlow: MutableStateFlow<Map<AccountId, Boolean>> = MutableStateFlow(emptyMap())
private val onWalletSelected = Channel<UserWalletId>()
// todo swap call new api result
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
override fun onDismiss() = marketBlockDelegate.addToPortfolioSlot.dismiss()
override fun onSuccess(addedToken: CryptoCurrency) {
val newToken = addedToken to ChooseTokenAnalyticsPayload.IsSearched(isSearchingState)
bridge.onNewTokenAdded(newToken)
marketBlockDelegate.addToPortfolioSlot.dismiss()
}
}
val stateOld: StateFlow<SwapSelectTokenStateHolder?> = combineUIOld()
private val contentState: StateFlow<ChooseTokenUM?> = combineUI()
@ -103,6 +90,27 @@ internal class ChooseTokenModel @Inject constructor(
initialValue = ChooseTokenFullUM(initialState.value, contentState.value),
)
init {
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { marketBlockDelegate.addToPortfolioSlot.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { addedResult ->
val isSearched = ChooseTokenAnalyticsPayload.IsSearched(isSearchingState)
val newToken = addedResult.addedCurrency.currency to isSearched
bridge.onNewTokenAdded(newToken)
val chooseTokenResult = ChooseTokenResult(
currency = addedResult.addedCurrency,
account = addedResult.account,
wallet = addedResult.wallet,
analyticsPayload = setOf(isSearched),
)
bridge.onCurrencyChosen(chooseTokenResult)
marketBlockDelegate.addToPortfolioSlot.dismiss()
}
.launchIn(modelScope)
}
@Suppress("UnusedPrivateMember")
private fun combineUIOld(): StateFlow<SwapSelectTokenStateHolder?> = combine(
flow = bridge.currenciesGroup,

View file

@ -20,14 +20,11 @@ import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class MarketBlockDelegate @AssistedInject constructor(
@ -40,12 +37,15 @@ internal class MarketBlockDelegate @AssistedInject constructor(
@Assisted private val screensSourcesName: String,
) {
private val addToPortfolioJobHolder = JobHolder()
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
val addToPortfolioSlot: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
var addToPortfolioManager: AddToPortfolioManager? = null
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings.ChooseToken,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName),
)
val marketsStateFlow: Flow<SwapMarketState> = searchQueryState
// Switch between default and search market flows
@ -177,45 +177,34 @@ internal class MarketBlockDelegate @AssistedInject constructor(
}
private fun addToPortfolioItem(item: MarketsListItemUM) {
modelScope.launch {
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
?: searchMarketsListManager.getTokenMarketById(item.id)
?: return@launch
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
?: searchMarketsListManager.getTokenMarketById(item.id) ?: return
val param = tokenMarket.toSerializableParam()
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
val param = tokenMarket.toSerializableParam()
val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot }
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,
hotExcludedBlockchains = hotWalletExcludedBlockchains,
hasOnlyHotWallets = hasOnlyHotWallets,
)
}?.map { network ->
TokenMarketInfo.Network(
networkId = network.networkId,
isExchangeable = false,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
}.orEmpty()
val networks = tokenMarket.networks?.filter { network ->
BlockchainUtils.isSupportedNetworkId(
networkId = network.networkId,
coinId = tokenMarket.id.value,
contractAddress = network.contractAddress,
excludedBlockchains = excludedBlockchains,
hotExcludedBlockchains = hotWalletExcludedBlockchains,
hasOnlyHotWallets = hasOnlyHotWallets,
)
}?.map { network ->
TokenMarketInfo.Network(
networkId = network.networkId,
isExchangeable = false,
contractAddress = network.contractAddress,
decimalCount = network.decimalCount,
)
}.orEmpty()
addToPortfolioManager = addToPortfolioManagerFactory
.create(
scope = modelScope,
token = param,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName),
).apply {
setTokenNetworks(networks)
}
addToPortfolioManager.setTokenNetworks(networks)
addToPortfolioManager.setTokenParams(param)
addToPortfolioManager?.state
?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd }
?.run { addToPortfolioSlot.activate(AddToPortfolioRoute) }
}.saveIn(addToPortfolioJobHolder)
addToPortfolioSlot.activate(AddToPortfolioRoute)
}
@AssistedFactory