Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-29 18:02:22 +04:00
parent 7095ceec0f
commit 82506a1c9c
29 changed files with 239 additions and 997 deletions

View file

@ -1,25 +1,19 @@
package com.tangem.feature.swap.choosetoken.api
package com.tangem.features.commonfeatures.api.choosetoken
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.presentation.R
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.api.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// todo swap move to common-features module
interface ChooseTokenBridge : ChooseTokenBridgeLegacy, ChooseTokenBridgeInternal {
interface ChooseTokenBridge : ChooseTokenBridgeInternal {
val onCurrencyChosen: Channel<ChooseTokenResult>
val onClose: Channel<Unit>
@ -84,33 +78,6 @@ interface ChooseTokenBridgeInternal {
}
}
// todo swap legacy api, remove
interface ChooseTokenBridgeLegacy : ChooseTokenBridgeInternal {
val onTokenSelected: Channel<ChooseTokenResultOld>
val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>>
val currenciesGroup: Flow<CurrenciesGroup>
fun onTokenSelected(result: ChooseTokenResultOld) {
onTokenSelected.trySend(result)
onSearchQuery(SearchQuery.Empty)
}
fun onNewTokenAdded(addedToken: Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>) {
onNewTokenAdded.trySend(addedToken)
onSearchQuery(SearchQuery.Empty)
}
fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup)
}
data class ChooseTokenResultOld(
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val account: Account,
val isSearched: Boolean,
)
data class ChooseTokenResult(
val currency: CryptoCurrencyStatus,
val account: AccountStatus,

View file

@ -1,9 +1,9 @@
package com.tangem.feature.swap.choosetoken.api
package com.tangem.features.commonfeatures.api.choosetoken
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
internal interface ChooseTokenComponent : ComposableContentComponent {
interface ChooseTokenComponent : ComposableContentComponent {
data class Params(
val bridge: ChooseTokenBridge,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.api.model
package com.tangem.features.commonfeatures.api.choosetoken.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM

View file

@ -18,8 +18,6 @@ tasks.withType<Test>().configureEach {
dependencies {
/** Api */
implementation(projects.features.commonFeatures.api)
// todo swap delete after move portfolio selector
implementation(projects.features.account.api)
implementation(projects.features.wallet.api)
implementation(projects.features.tokenRecieve.api)
@ -57,6 +55,7 @@ dependencies {
/** Common */
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
implementation(projects.common.uiMarkets)
implementation(projects.common.routing)

View file

@ -1,20 +1,17 @@
package com.tangem.feature.swap.choosetoken.impl
package com.tangem.features.commonfeatures.impl.choosetoken
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.UserWalletId
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge.Settings
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResultOld
import com.tangem.feature.swap.choosetoken.api.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY
import com.tangem.feature.swap.choosetoken.impl.model.PortfolioFullBlockDelegate
import com.tangem.feature.swap.choosetoken.impl.model.PortfolioListBlockDelegate
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge.Settings
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.impl.choosetoken.model.PortfolioFullBlockDelegate
import com.tangem.features.commonfeatures.impl.choosetoken.model.PortfolioListBlockDelegate
import com.tangem.features.commonfeatures.impl.choosetoken.model.ChooseTokenModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -31,14 +28,11 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor(
) : ChooseTokenBridge {
override val onCurrencyChosen: Channel<ChooseTokenResult> = Channel()
override val onTokenSelected: Channel<ChooseTokenResultOld> = Channel()
override val onNewTokenAdded: Channel<Pair<CryptoCurrency, ChooseTokenAnalyticsPayload.IsSearched>> = Channel()
override val onClose: Channel<Unit> = Channel()
private val onSearchQuery: Channel<SearchQuery> = Channel()
override val searchQueryState: StateFlow<SearchQuery> = onSearchQuery.receiveAsFlow()
.debounce(DEBOUNCE_SEARCH_DELAY)
.debounce(ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY)
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = SearchQuery.Empty)
private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create(
@ -59,9 +53,6 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor(
override val fullPortfolioBlock: StateFlow<ChooseTokenPortfolioFullBlockUM?>
get() = portfolioFullBlockDelegate.fullPortfolioBlock
private val _currenciesGroupFlow = MutableStateFlow<CurrenciesGroup?>(null)
override val currenciesGroup: Flow<CurrenciesGroup> = _currenciesGroupFlow.filterNotNull()
init {
portfolioListBlockDelegate.onTokenChosen.receiveAsFlow()
.onEach { chooseResult -> onCurrencyChosen(chooseResult) }
@ -86,10 +77,6 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor(
onSearchQuery(SearchQuery.Empty)
}
override fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) {
_currenciesGroupFlow.update { currenciesGroup }
}
@AssistedFactory
interface Factory : ChooseTokenBridge.Factory {
override fun create(

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl
package com.tangem.features.commonfeatures.impl.choosetoken
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -10,16 +10,16 @@ import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenScreen
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.ui.SwapSelectTokenScreen
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.impl.choosetoken.model.ChooseTokenModel
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenScreen
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.serialization.Serializable
internal class DefaultChooseTokenComponent @AssistedInject constructor(
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
@ -39,15 +39,7 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val stateOld by model.stateOld.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
stateOld?.let { stateHolder ->
SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() })
bottomSheet.child?.instance?.BottomSheet()
// if old shown we should not show new screen
return
}
val state by model.state.collectAsStateWithLifecycle()
ChooseTokenScreen(state = state)
bottomSheet.child?.instance?.BottomSheet()
@ -74,4 +66,7 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor(
private companion object {
const val BOTTOM_SHEET_SLOT_KEY = "choosePortfolioTokenBottomSheetSlot"
}
}
}
@Serializable
internal data object AddToPortfolioRoute : Route

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.api
package com.tangem.features.commonfeatures.impl.choosetoken
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import javax.inject.Inject
// todo swap move to some common module
class SettingContextUseCase @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl.converter
package com.tangem.features.commonfeatures.impl.choosetoken.converter
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
import com.tangem.common.ui.account.TokensListPortfolioItemConverter
@ -23,11 +23,11 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.choosetoken.impl.model.ClickIntents
import com.tangem.feature.swap.presentation.R
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
import com.tangem.features.commonfeatures.impl.choosetoken.model.ClickIntents
import com.tangem.features.commonfeatures.impl.R
import kotlinx.collections.immutable.toPersistentList
internal class ChooseTokenListItemConverter(

View file

@ -1,6 +1,6 @@
package com.tangem.feature.swap.choosetoken.impl.converter
package com.tangem.features.commonfeatures.impl.choosetoken.converter
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
import com.tangem.utils.transformer.Transformer
internal class SearchBarToggleTransformer(private val isActive: Boolean) : Transformer<ChooseTokenInitialUM> {

View file

@ -1,6 +1,6 @@
package com.tangem.feature.swap.choosetoken.impl.converter
package com.tangem.features.commonfeatures.impl.choosetoken.converter
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
import com.tangem.utils.transformer.Transformer
internal class SearchBarUpdateQueryTransformer(private val newQuery: String) : Transformer<ChooseTokenInitialUM> {

View file

@ -1,12 +1,12 @@
package com.tangem.feature.swap.choosetoken.impl.di
package com.tangem.features.commonfeatures.impl.choosetoken.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenBridge
import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenComponent
import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.impl.choosetoken.DefaultChooseTokenBridge
import com.tangem.features.commonfeatures.impl.choosetoken.DefaultChooseTokenComponent
import com.tangem.features.commonfeatures.impl.choosetoken.model.ChooseTokenModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -1,11 +1,11 @@
package com.tangem.feature.swap.models.market
package com.tangem.features.commonfeatures.impl.choosetoken.market
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.models.market.converter.SwapMarketsTokenItemConverter
import com.tangem.features.commonfeatures.impl.choosetoken.market.converter.SwapMarketsTokenItemConverter
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.models.market.converter
package com.tangem.features.commonfeatures.impl.choosetoken.market.converter
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartRawData
@ -16,7 +16,7 @@ import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.feature.swap.presentation.R
import com.tangem.features.commonfeatures.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.models.market.state
package com.tangem.features.commonfeatures.impl.choosetoken.market.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl.model
package com.tangem.features.commonfeatures.impl.choosetoken.model
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.ModelScoped
@ -6,18 +6,18 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.AccountId
import com.tangem.feature.swap.choosetoken.api.*
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarToggleTransformer
import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarUpdateQueryTransformer
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenFullUM
import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM
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.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarToggleTransformer
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarUpdateQueryTransformer
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenFullUM
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
import com.tangem.features.commonfeatures.api.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -26,7 +26,6 @@ import javax.inject.Inject
@ModelScoped
internal class ChooseTokenModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val settingContextUseCase: SettingContextUseCase,
marketBlockDelegateFactory: MarketBlockDelegate.Factory,
paramsContainer: ParamsContainer,
) : Model() {
@ -52,10 +51,6 @@ internal class ChooseTokenModel @Inject constructor(
flowOf(null)
}
private val expandedAccountsFlow: MutableStateFlow<Map<AccountId, Boolean>> = MutableStateFlow(emptyMap())
val stateOld: StateFlow<SwapSelectTokenStateHolder?> = combineUIOld()
private val initialState: MutableStateFlow<ChooseTokenInitialUM> = MutableStateFlow(getInitState())
val state: StateFlow<ChooseTokenFullUM> = combine(
flow = initialState,
@ -85,8 +80,6 @@ internal class ChooseTokenModel @Inject constructor(
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,
@ -99,46 +92,6 @@ internal class ChooseTokenModel @Inject constructor(
.launchIn(modelScope)
}
@Suppress("UnusedPrivateMember")
private fun combineUIOld(): StateFlow<SwapSelectTokenStateHolder?> = combine(
flow = bridge.currenciesGroup,
flow2 = settingContextUseCase.invoke(),
flow3 = marketsStateFlow,
flow4 = expandedAccountsFlow,
transform = { currenciesGroup, settingContext, marketState, expandedAccounts ->
val isAccountsMode = settingContext.isAccountsMode
val appCurrency = settingContext.appCurrency
val isBalanceHidden = settingContext.isBalanceHidden
TokensDataConverter(
onSearchEntered = { query -> bridge.onSearchQuery(query) },
onTokenClick = { account, cryptoCurrencyStatus ->
val result = ChooseTokenResultOld(
account = account,
cryptoCurrencyStatus = cryptoCurrencyStatus,
isSearched = searchQueryState.isSearchingState,
)
bridge.onTokenSelected(result)
},
onAccountClick = { account ->
expandedAccountsFlow.update { expandedList ->
val hasSavedAccount = expandedList[account.accountId]
val isExpanded = hasSavedAccount == true
expandedList + (account.accountId to !isExpanded)
}
},
expandedAccounts = expandedAccounts,
tokensDataState = currenciesGroup,
isBalanceHidden = isBalanceHidden,
isAccountsMode = isAccountsMode,
appCurrency = appCurrency,
marketState = requireNotNull(marketState),
).transform()
},
)
.flowOn(dispatchers.default)
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = null)
fun onBackClicked() {
bridge.onClose()
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl.model
package com.tangem.features.commonfeatures.impl.choosetoken.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
@ -14,12 +14,12 @@ import com.tangem.domain.markets.toSerializableParam
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.impl.choosetoken.AddToPortfolioRoute
import com.tangem.features.commonfeatures.impl.choosetoken.market.MarketsListBatchFlowManager
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import dagger.assisted.Assisted
@ -27,6 +27,9 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlin.collections.filter
import kotlin.collections.map
import kotlin.collections.orEmpty
@Suppress("LongParameterList")
internal class MarketBlockDelegate @AssistedInject constructor(

View file

@ -1,19 +1,20 @@
package com.tangem.feature.swap.choosetoken.impl.model
package com.tangem.features.commonfeatures.impl.choosetoken.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase
import com.tangem.feature.swap.choosetoken.api.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.feature.swap.choosetoken.api.model.WalletListUM
import com.tangem.feature.swap.choosetoken.api.model.WalletTabUM
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM
import com.tangem.features.commonfeatures.impl.choosetoken.SettingContextUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -60,8 +61,10 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor(
}
private fun buildFlow() = flow {
val walletsFlow = getWalletsUseCase.invokeAsMap()
.map { wallets -> wallets.filterNot { (_, wallet) -> wallet.isLocked } }
val fullPortfolioBlockFlow = combine(
flow = getWalletsUseCase.invokeAsMap(),
flow = walletsFlow,
flow2 = portfolioListBlockDelegate.portfolioList,
flow3 = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(),
flow4 = settingContextUseCase.invoke(),

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl.model
package com.tangem.features.commonfeatures.impl.choosetoken.model
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.domain.account.models.AccountStatusList
@ -11,14 +11,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.choosetoken.impl.converter.ChooseTokenListItemConverter
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
import com.tangem.features.commonfeatures.impl.choosetoken.SettingContextUseCase
import com.tangem.features.commonfeatures.impl.choosetoken.converter.ChooseTokenListItemConverter
import com.tangem.utils.extensions.mapNotNullValues
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.choosetoken.impl.ui
package com.tangem.features.commonfeatures.impl.choosetoken.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -45,14 +45,12 @@ import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.feature.swap.choosetoken.api.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.choosetoken.api.model.WalletListUM
import com.tangem.feature.swap.choosetoken.api.model.WalletTabUM
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.ui.market.swapMarketsListItems
import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@ -254,7 +252,8 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
style = TangemTheme.typography.button,
)
if (state.count != null) {
val count = state.count
if (count != null) {
Spacer(modifier = Modifier.width(8.dp))
Box(
@ -265,7 +264,7 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
contentAlignment = Alignment.Center,
) {
Text(
text = state.count.resolveReference(),
text = count.resolveReference(),
color = countTextColor,
style = TangemTheme.typography.caption1,
)
@ -483,7 +482,7 @@ private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider<Choose
accounts.size,
),
),
marketsBlock = SwapSelectTokenPreviewProvider.defaultState.marketsState,
marketsBlock = SwapSelectTokenPreviewProvider.marketState,
),
ChooseTokenFullUM(
initialUM = initialUM,

View file

@ -1,9 +1,9 @@
package com.tangem.feature.swap.choosetoken.impl.ui
package com.tangem.features.commonfeatures.impl.choosetoken.ui
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.choosetoken.api.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
internal data class ChooseTokenFullUM(
val initialUM: ChooseTokenInitialUM,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.ui.market
package com.tangem.features.commonfeatures.impl.choosetoken.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
@ -17,8 +17,8 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
item {

View file

@ -0,0 +1,125 @@
package com.tangem.features.commonfeatures.impl.choosetoken.ui
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
internal object SwapSelectTokenPreviewProvider {
private const val CHART_VALUE_1 = 0.4
private const val CHART_VALUE_2 = 0.2
private const val CHART_VALUE_3 = 0.1
private const val CHART_VALUE_4 = 2.0
private const val CHART_VALUE_5 = 5.0
private const val CHART_VALUE_6 = 3.0
private const val TOTAL_ITEMS = 322
private val PREVIEW_CHART_DATA = MarketChartRawData(
y = persistentListOf(
CHART_VALUE_1,
CHART_VALUE_2,
CHART_VALUE_1,
CHART_VALUE_3,
CHART_VALUE_1,
CHART_VALUE_4,
CHART_VALUE_5,
CHART_VALUE_3,
CHART_VALUE_4,
CHART_VALUE_4,
CHART_VALUE_6,
),
)
val marketState = SwapMarketState.Content(
items = createPreviewMarketItems(),
loadMore = { },
onItemClick = { },
visibleIdsChanged = { },
total = TOTAL_ITEMS,
marketsTitle = TextReference.Res(R.string.feed_trending_now),
shouldAssetsCount = false,
)
private fun createPreviewMarketItems() = listOf(
createMarketItem(
id = "1",
iconUrl = "",
ratingPosition = "10",
marketCap = "$6.233 B",
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "2",
ratingPosition = "10",
marketCap = "$6.233 B",
trendType = PriceChangeType.NEUTRAL,
chartData = null,
),
createMarketItem(
id = "3",
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
ratingPosition = "10",
marketCap = "$6.23348172384781234 B",
trendType = PriceChangeType.DOWN,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "4",
ratingPosition = "10",
marketCap = null,
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "5",
ratingPosition = null,
marketCap = "$6.233 B",
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "6",
ratingPosition = null,
marketCap = null,
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
).toImmutableList()
private fun createMarketItem(
id: String,
name: String = "Bitcoin",
iconUrl: String? = null,
ratingPosition: String?,
marketCap: String?,
trendType: PriceChangeType,
chartData: MarketChartRawData?,
) = MarketsListItemUM(
id = CryptoCurrency.RawID(id),
name = name,
currencySymbol = "BTC",
iconUrl = iconUrl,
ratingPosition = ratingPosition,
marketCap = marketCap,
price = MarketsListItemUM.Price(
text = "31 285.72$",
annotated = stringReference("31 285.72$"),
fiatPrice = BigDecimal("123123"),
),
trendPercentText = "12.43%",
trendType = trendType,
chartData = chartData,
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
updateTimestamp = 0,
)
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
import com.tangem.feature.swap.model.SwapModel
import com.tangem.feature.swap.models.SwapPermissionUM

View file

@ -1,81 +0,0 @@
package com.tangem.feature.swap.converters
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.plus
import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
internal class TokensDataConverter(
onTokenClick: (Account, CryptoCurrencyStatus) -> Unit,
onAccountClick: (Account) -> Unit,
private val expandedAccounts: Map<AccountId, Boolean>,
private val onSearchEntered: (String) -> Unit,
private val tokensDataState: CurrenciesGroup,
private val isBalanceHidden: Boolean,
private val isAccountsMode: Boolean,
private val appCurrency: AppCurrency,
private val marketState: SwapMarketState,
) {
private val accountListItemConverter = AccountTokenItemConverter(
appCurrency = appCurrency,
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
onTokenItemClick = onTokenClick,
onAccountItemClick = onAccountClick,
expandedAccounts = expandedAccounts,
)
fun transform(): SwapSelectTokenStateHolder {
val accountList = tokensDataState.accountCurrencyList
return SwapSelectTokenStateHolder(
tokensListData = if (isAccountsMode) {
val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList()
val totalTokensCount = portfolioList.sumOf { it.tokens.size }
if (totalTokensCount > 0) {
TokenListUMData.AccountList(
tokensList = portfolioList,
totalTokensCount = totalTokensCount,
)
} else {
TokenListUMData.EmptyList
}
} else {
val tokensList = accountList.flatMap { (_, currencyList) ->
currencyList.asSequence().map { accountSwapCurrency ->
accountListItemConverter.createAvailableItemConverter(accountSwapCurrency.account)
.convert(accountSwapCurrency.cryptoCurrencyStatus)
}.map(TokensListItemUM::Token).toPersistentList()
}.toPersistentList()
if (tokensList.isNotEmpty()) {
TokenListUMData.TokenList(
tokensList = persistentListOf(
TokensListItemUM.GroupTitle(
id = "available_tokens_title",
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
),
) + tokensList,
totalTokensCount = tokensList.size,
)
} else {
TokenListUMData.EmptyList
}
},
marketsState = marketState,
onSearchEntered = onSearchEntered,
isBalanceHidden = isBalanceHidden,
isAfterSearch = tokensDataState.isAfterSearch,
)
}
}

View file

@ -69,9 +69,9 @@ import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNet
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.analytics.SwapQuotePerformanceTracker
import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload
import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge
import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
import com.tangem.feature.swap.domain.AllowPermissionsHandler

View file

@ -1,7 +0,0 @@
package com.tangem.feature.swap.models
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal data object AddToPortfolioRoute : Route

View file

@ -1,22 +0,0 @@
package com.tangem.feature.swap.models
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.models.market.state.SwapMarketState
internal data class SwapSelectTokenStateHolder(
val marketsState: SwapMarketState,
val tokensListData: TokenListUMData,
val isBalanceHidden: Boolean,
val isAfterSearch: Boolean,
val onSearchEntered: (String) -> Unit,
)
internal val SwapSelectTokenStateHolder.isNotFoundState: Boolean
get() =
tokensListData.tokensList.isEmpty() && isAfterSearch &&
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
internal val SwapSelectTokenStateHolder.isEmptyState: Boolean
get() =
tokensListData.tokensList.isEmpty() && !isAfterSearch &&
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading

View file

@ -1,457 +0,0 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.ExpandableSearchView
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BuyTokenScreenTestTags
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.isEmptyState
import com.tangem.feature.swap.models.isNotFoundState
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.ui.market.swapMarketsListItems
import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider
import kotlinx.collections.immutable.ImmutableList
private const val LOAD_MORE_BUFFER = 25
@Composable
internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) {
BackHandler(onBack = onBack)
Scaffold(
modifier = Modifier
.systemBarsPadding()
.background(color = TangemTheme.colors.background.secondary),
content = { padding ->
val modifier = Modifier.padding(padding)
when {
state.isNotFoundState -> TokensNotFound(modifier)
state.isEmptyState -> EmptyTokensList(modifier)
state.marketsState != null -> ListOfTokensWithMarkets(
state = state,
marketsState = state.marketsState,
modifier = modifier,
)
else -> ListOfTokens(state = state, modifier = modifier)
}
},
topBar = {
ExpandableSearchView(
title = stringResourceSafe(R.string.common_choose_token),
onBackClick = onBack,
placeholderSearchText = stringResourceSafe(id = R.string.common_search_tokens),
onSearchChange = state.onSearchEntered,
onSearchDisplayClose = { state.onSearchEntered("") },
subtitle = stringResourceSafe(id = R.string.express_exchange_token_list_subtitle),
)
},
)
}
@Composable
private fun EmptyTokensList(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxSize(),
) {
Column(modifier = Modifier.align(Alignment.Center)) {
Image(
modifier = Modifier
.size(TangemTheme.dimens.size64)
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.ic_no_token_44),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentDescription = null,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing30)
.align(Alignment.CenterHorizontally),
text = stringResourceSafe(id = R.string.exchange_tokens_empty_tokens),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
}
@Composable
private fun TokensNotFound(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxSize(),
) {
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32)
.padding(horizontal = TangemTheme.dimens.spacing30)
.align(Alignment.TopCenter),
text = stringResourceSafe(id = R.string.express_token_list_empty_search),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) {
val screenBackgroundColor = TangemTheme.colors.background.secondary
LazyColumn(
modifier = modifier
.background(color = screenBackgroundColor)
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
tokensListItems(
tokensListData = state.tokensListData,
isBalanceHidden = state.isBalanceHidden,
)
}
}
@Composable
private fun ListOfTokensWithMarkets(
state: SwapSelectTokenStateHolder,
marketsState: SwapMarketState,
modifier: Modifier = Modifier,
) {
val screenBackgroundColor = TangemTheme.colors.background.secondary
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = modifier
.background(color = screenBackgroundColor)
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.CenterHorizontally,
state = lazyListState,
) {
if (state.tokensListData !is TokenListUMData.EmptyList) {
assetsTitle(count = state.tokensListData.totalTokensCount, showCount = marketsState.shouldAssetsCount)
}
tokensListItems(
tokensListData = state.tokensListData,
isBalanceHidden = state.isBalanceHidden,
)
item("spacer_before_markets") {
SpacerH(32.dp)
}
swapMarketsListItems(marketsState)
}
(marketsState as? SwapMarketState.Content)?.let { content ->
VisibleItemsTracker(
lazyListState = lazyListState,
marketState = content,
)
InfiniteListHandler(
listState = lazyListState,
buffer = LOAD_MORE_BUFFER,
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(content) {
{
content.loadMore()
true
}
},
)
}
}
@Composable
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
val visibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo
.mapNotNull { itemInfo ->
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
}
}
}
LaunchedEffect(visibleItems) {
marketState.visibleIdsChanged(visibleItems)
}
}
private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) {
item(key = "assets_title") {
Text(
text = buildAnnotatedString {
append(stringResourceSafe(R.string.swap_your_assets_title))
if (showCount) {
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
append(" $count")
}
}
},
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
),
)
}
}
private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) {
when (tokensListData) {
is TokenListUMData.AccountList -> {
tokensListData.tokensList.forEachIndexed { index, item ->
portfolioTokensList(
portfolio = item,
isBalanceHidden = isBalanceHidden,
portfolioIndex = index,
modifier = Modifier,
)
}
}
is TokenListUMData.TokenList -> {
tokensList(
items = tokensListData.tokensList,
isBalanceHidden = isBalanceHidden,
)
}
TokenListUMData.EmptyList -> Unit
}
}
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
itemsIndexed(
items = items,
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TokenListItem(
state = item,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = items.lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
)
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
.semantics { lazyListItemPosition = index },
)
},
)
}
internal fun LazyListScope.portfolioTokensList(
portfolio: TokensListItemUM.Portfolio,
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
val lastIndex = tokens.lastIndex.inc()
portfolioItem(
portfolio = portfolio,
modifier = modifier,
portfolioIndex = portfolioIndex,
isBalanceHidden = isBalanceHidden,
)
itemsIndexed(
items = tokens,
key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, token ->
val indexWithHeader = tokenIndex.inc()
SlideInItemVisibility(
currentIndex = tokenIndex,
lastIndex = lastIndex,
modifier = modifier
.testModifier(indexWithHeader)
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = TangemTheme.dimens.radius14,
currentIndex = indexWithHeader,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
visible = isExpanded,
) {
val innerModifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier
PortfolioTokensListItem(
state = token,
isBalanceHidden = isBalanceHidden,
modifier = innerModifier,
)
}
},
)
}
@Suppress("MagicNumber")
private fun LazyListScope.portfolioItem(
portfolio: TokensListItemUM.Portfolio,
modifier: Modifier,
portfolioIndex: Int,
isBalanceHidden: Boolean,
) {
val tokens = portfolio.tokens
val isExpanded = portfolio.isExpanded
val lastIndex = when {
isExpanded && tokens.isEmpty() -> 1
isExpanded -> tokens.lastIndex.inc()
else -> 0
}
item(
key = "account-${portfolio.id}",
contentType = "account-content",
) {
// Snap immediately on expand; on collapse, hold until all child items finish
// their shrink animation, then snap to fully-rounded shape.
val effectiveLastIndex by animateIntAsState(
targetValue = lastIndex,
animationSpec = if (lastIndex != 0) {
snap()
} else {
snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150)
},
label = "lastIndex",
)
PortfolioListItem(
state = portfolio,
isBalanceHidden = isBalanceHidden,
modifier = modifier
.testModifier(portfolioIndex)
.roundedShapeItemDecoration(
currentIndex = 0,
radius = TangemTheme.dimens.radius14,
lastIndex = effectiveLastIndex,
backgroundColor = TangemTheme.colors.background.primary,
),
)
}
}
private fun Modifier.testModifier(index: Int): Modifier = this
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = index }
@Suppress("MagicNumber")
@Composable
internal fun SlideInItemVisibility(
visible: Boolean,
currentIndex: Int,
lastIndex: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val maxDelay = 250
val delayEnter = minOf(50 * currentIndex, maxDelay)
val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay)
AnimatedVisibility(
modifier = modifier,
visible = visible,
enter = expandVertically(
tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing),
expandFrom = Alignment.Top,
) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)),
exit = shrinkVertically(
tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing),
shrinkTowards = Alignment.Top,
) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)),
) {
content()
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SwapSelectTokenScreen_Preview(
@PreviewParameter(SwapSelectTokenScreenPreviewProvider::class) params: SwapSelectTokenStateHolder,
) {
TangemThemePreview {
SwapSelectTokenScreen(
state = params,
onBack = {},
)
}
}
private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider<SwapSelectTokenStateHolder> {
override val values: Sequence<SwapSelectTokenStateHolder>
get() = sequenceOf(
// Content state with tokens and markets
SwapSelectTokenPreviewProvider.defaultState,
// Empty state
SwapSelectTokenStateHolder(
tokensListData = TokenListUMData.EmptyList,
marketsState = SwapMarketState.DefaultLoading,
isAfterSearch = false,
isBalanceHidden = false,
onSearchEntered = {},
),
// Not found state
SwapSelectTokenStateHolder(
tokensListData = TokenListUMData.EmptyList,
marketsState = SwapMarketState.SearchLoading,
isAfterSearch = true,
isBalanceHidden = false,
onSearchEntered = {},
),
)
}
// endregion

View file

@ -1,221 +0,0 @@
package com.tangem.feature.swap.ui.preview
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.choosetoken.api.model.TokenListUMData
import com.tangem.feature.swap.models.SwapSelectTokenStateHolder
import com.tangem.feature.swap.models.market.state.SwapMarketState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal object SwapSelectTokenPreviewProvider {
private const val CHART_VALUE_1 = 0.4
private const val CHART_VALUE_2 = 0.2
private const val CHART_VALUE_3 = 0.1
private const val CHART_VALUE_4 = 2.0
private const val CHART_VALUE_5 = 5.0
private const val CHART_VALUE_6 = 3.0
private const val TOTAL_ITEMS = 322
private val PREVIEW_CHART_DATA = MarketChartRawData(
y = persistentListOf(
CHART_VALUE_1,
CHART_VALUE_2,
CHART_VALUE_1,
CHART_VALUE_3,
CHART_VALUE_1,
CHART_VALUE_4,
CHART_VALUE_5,
CHART_VALUE_3,
CHART_VALUE_4,
CHART_VALUE_4,
CHART_VALUE_6,
),
)
private val tokenItemState = TokenItemState.Content(
id = "1",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "34 496,75 \$",
priceChangePercent = "0,43 %",
type = PriceChangeType.DOWN,
),
onItemClick = {},
onItemLongClick = {},
)
private val textContentTokensState = persistentListOf(
TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")),
TokensListItemUM.Token(state = tokenItemState),
TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")),
TokensListItemUM.Token(
state = tokenItemState.copy(
id = "2",
titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "1 799,41 \$",
priceChangePercent = "5,16 %",
type = PriceChangeType.UP,
),
),
),
TokensListItemUM.Token(
state = TokenItemState.Unreachable(
id = "3",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
onItemClick = {},
onItemLongClick = {},
),
),
TokensListItemUM.Token(
state = tokenItemState.copy(
id = "4",
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "0.01 \$",
priceChangePercent = "1,34 %",
type = PriceChangeType.DOWN,
),
),
),
)
private val marketState = SwapMarketState.Content(
items = createPreviewMarketItems(),
loadMore = { },
onItemClick = { },
visibleIdsChanged = { },
total = TOTAL_ITEMS,
marketsTitle = TextReference.Res(R.string.feed_trending_now),
shouldAssetsCount = false,
)
val defaultState = SwapSelectTokenStateHolder(
tokensListData = TokenListUMData.AccountList(
tokensList = persistentListOf(
TokensListItemUM.Portfolio(
content = PortfolioItemContentUM.Tokens(
tokens = textContentTokensState.filterIsInstance<PortfolioTokensListItemUM>()
.toPersistentList(),
),
isExpanded = false,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
content = PortfolioItemContentUM.Tokens(
tokens = textContentTokensState.filterIsInstance<PortfolioTokensListItemUM>()
.toPersistentList(),
),
isExpanded = true,
isCollapsable = true,
tokenItemUM = AccountItemPreviewData.accountItem,
),
),
totalTokensCount = TOTAL_ITEMS,
),
isAfterSearch = false,
isBalanceHidden = false,
onSearchEntered = {},
marketsState = marketState,
)
private fun createPreviewMarketItems() = listOf(
createMarketItem(
id = "1",
iconUrl = "",
ratingPosition = "10",
marketCap = "$6.233 B",
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "2",
ratingPosition = "10",
marketCap = "$6.233 B",
trendType = PriceChangeType.NEUTRAL,
chartData = null,
),
createMarketItem(
id = "3",
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
ratingPosition = "10",
marketCap = "$6.23348172384781234 B",
trendType = PriceChangeType.DOWN,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "4",
ratingPosition = "10",
marketCap = null,
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "5",
ratingPosition = null,
marketCap = "$6.233 B",
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
createMarketItem(
id = "6",
ratingPosition = null,
marketCap = null,
trendType = PriceChangeType.UP,
chartData = PREVIEW_CHART_DATA,
),
).toImmutableList()
private fun createMarketItem(
id: String,
name: String = "Bitcoin",
iconUrl: String? = null,
ratingPosition: String?,
marketCap: String?,
trendType: PriceChangeType,
chartData: MarketChartRawData?,
) = MarketsListItemUM(
id = CryptoCurrency.RawID(id),
name = name,
currencySymbol = "BTC",
iconUrl = iconUrl,
ratingPosition = ratingPosition,
marketCap = marketCap,
price = MarketsListItemUM.Price(
text = "31 285.72$",
annotated = stringReference("31 285.72$"),
fiatPrice = BigDecimal("123123"),
),
trendPercentText = "12.43%",
trendType = trendType,
chartData = chartData,
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
updateTimestamp = 0,
)
}