Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 15:56:27 +03:00
commit e388c14eb9
184 changed files with 4338 additions and 1298 deletions

View file

@ -13,7 +13,6 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
@ -139,7 +138,19 @@ internal class ArchivedAccountListModel @Inject constructor(
val featureError = AccountFeatureError.ArchivedAccountList.FailedToRecoverAccount(cause = error)
logError(error = featureError)
messageSender.showErrorDialog(universalError = featureError, onDismiss = router::pop)
val messageText = resourceReference(
id = R.string.universal_error,
formatArgs = wrappedList(featureError.errorCode),
)
val message = DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),
message = messageText,
firstAction = EventMessageAction(
title = resourceReference(R.string.common_ok),
onClick = {},
),
)
messageSender.send(message)
}
private fun logError(error: AccountFeatureError, params: Map<String, String> = emptyMap()) {

View file

@ -21,10 +21,11 @@ import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.model.feed.FeedModelClickIntents
import com.tangem.features.feed.ui.EntryBottomSheetContent
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -60,7 +61,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
}
override fun onMarketOpenClick(sortBy: SortByTypeUM) {
innerRouter.push(FeedEntryChildFactory.Child.TokenList)
innerRouter.push(
route = FeedEntryChildFactory.Child.TokenList(
params = DefaultMarketsTokenListComponent.Params(
onBackClicked = { onChildBack() },
onTokenClick = { token, currency -> onMarketItemClick(token, currency) },
preselectedSortType = sortBy,
shouldAlwaysShowSearchBar = sortBy == SortByTypeUM.Rating,
),
),
)
}
override fun onArticleClick(articleId: Int) {

View file

@ -24,7 +24,7 @@ internal class FeedEntryChildFactory @Inject constructor() {
@Serializable
@Immutable
data object TokenList : Child
data class TokenList(val params: DefaultMarketsTokenListComponent.Params) : Child
@Serializable
@Immutable
@ -54,12 +54,7 @@ internal class FeedEntryChildFactory @Inject constructor() {
is Child.TokenList -> {
DefaultMarketsTokenListComponent(
appComponentContext = appComponentContext,
onTokenClick = { token, appCurrency ->
feedEntryClickIntents.onMarketItemClick(
token,
appCurrency,
)
},
params = child.params,
)
}
Child.NewsDetails -> {

View file

@ -1,27 +1,57 @@
package com.tangem.features.feed.components.market.list
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.model.market.list.MarketsListModel
import com.tangem.features.feed.ui.market.list.MarketsList
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import kotlinx.serialization.Serializable
@Suppress("UnusedPrivateProperty") // TODO will be remove in next PR
internal class DefaultMarketsTokenListComponent(
appComponentContext: AppComponentContext,
private val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)? = null,
private val params: Params,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val model: MarketsListModel = getOrCreateModel<MarketsListModel, Params>(params = params)
@Composable
override fun Title() {
val state by model.state.collectAsStateWithLifecycle()
TopBarWithSearch(state.searchBar)
}
@Composable
override fun Content(modifier: Modifier) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
MarketsList(
modifier = modifier,
state = state,
)
}
@Composable
override fun Footer() {
}
override fun Footer() = Unit
@Serializable
data class Params(
val onBackClicked: () -> Unit,
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
val preselectedSortType: SortByTypeUM,
val shouldAlwaysShowSearchBar: Boolean,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.model.feed.FeedComponentModel
import com.tangem.features.feed.model.market.list.MarketsListModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -17,4 +18,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(FeedComponentModel::class)
fun bindsFeedComponentModel(model: FeedComponentModel): Model
@Binds
@IntoMap
@ClassKey(MarketsListModel::class)
fun provideMarketsListModel(model: MarketsListModel): Model
}

View file

@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.state.MarketsListUM
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal

View file

@ -18,7 +18,7 @@ import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
@ -31,6 +31,7 @@ import kotlinx.coroutines.launch
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import javax.inject.Inject
import kotlin.collections.all
@Stable
@ModelScoped
@ -141,7 +142,9 @@ internal class FeedComponentModel @Inject constructor(
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
onActiveChange = {
if (it) params.feedClickIntents.onMarketOpenClick(SortByTypeUM.Rating)
},
),
feedListCallbacks = FeedListCallbacks(
onSearchClick = {},

View file

@ -2,7 +2,7 @@ package com.tangem.features.feed.model.feed
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
/**
* Callback interface for feed model navigation actions.

View file

@ -0,0 +1,329 @@
package com.tangem.features.feed.model.market.list
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
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.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.ShouldShowYieldModeMarketPromoUseCase
import com.tangem.domain.markets.TokenMarketListConfig
import com.tangem.domain.markets.toSerializableParam
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.promo.PromoRepository
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.model.market.list.analytics.MarketsListAnalyticsEvent
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
import com.tangem.features.feed.model.market.list.statemanager.MarketsListUMStateManager
import com.tangem.features.feed.ui.market.list.state.ListUM
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
import com.tangem.features.feed.ui.market.list.state.MarketsNotificationUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@ModelScoped
@Stable
@Suppress("LongParameterList", "PropertyUsedBeforeDeclaration")
internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
shouldShowYieldModeMarketPromoUseCase: ShouldShowYieldModeMarketPromoUseCase,
paramsContainer: ParamsContainer,
private val promoRepository: PromoRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val updateQuotesJob = JobHolder()
private val params = paramsContainer.require<DefaultMarketsTokenListComponent.Params>()
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
private val visibleItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
private val marketsListUMStateManager by lazy {
MarketsListUMStateManager(
currentVisibleIds = Provider { visibleItemIds.value },
onLoadMoreUiItems = { activeListManager.loadMore() },
visibleItemsChanged = { visibleItemIds.value = it },
onRetryButtonClicked = { activeListManager.reload() },
onTokenClick = { onTokenUIClicked(it) },
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
shouldAlwaysShowSearchBar = Provider { params.shouldAlwaysShowSearchBar },
preselectedSortType = Provider { params.preselectedSortType },
)
}
private val mainMarketsListManager by lazy {
MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
currentAppCurrency = Provider { currentAppCurrency.value },
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval },
currentSortByType = Provider { marketsListUMStateManager.selectedSortByType },
currentSearchText = Provider { null },
modelScope = modelScope,
dispatchers = dispatchers,
)
}
private val searchMarketsListManager by lazy {
MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
currentAppCurrency = Provider { currentAppCurrency.value },
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval },
currentSortByType = Provider { SortByTypeUM.Rating },
currentSearchText = Provider { marketsListUMStateManager.searchQuery },
modelScope = modelScope,
dispatchers = dispatchers,
)
}
private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager
val isVisibleOnScreen = MutableStateFlow(false)
val state = marketsListUMStateManager.state.asStateFlow()
init {
modelScope.launch {
marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode ->
if (isInSearchMode) {
combine(
flow = searchMarketsListManager.uiItems,
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
flow3 = searchMarketsListManager.isSearchNotFoundState,
flow4 = shouldShowYieldModeMarketPromoUseCase(
appCurrency = currentAppCurrency.value,
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
),
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo ->
MarketsItemsData(
items = uiItems,
isInErrorState = isInInitialLoadingErrorState,
isSearchNotFound = isSearchNotFoundState,
shouldShowYieldModePromo = isYieldModePromo,
)
}
} else {
combine(
flow = mainMarketsListManager.uiItems,
flow2 = mainMarketsListManager.isInInitialLoadingErrorState,
flow3 = shouldShowYieldModeMarketPromoUseCase(
appCurrency = currentAppCurrency.value,
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
),
) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo ->
MarketsItemsData(
items = uiItems,
isInErrorState = isInInitialLoadingErrorState,
isSearchNotFound = false,
shouldShowYieldModePromo = shouldShowYieldModePromo,
)
}
}
}.collect { marketsItemsData ->
val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo
if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown())
}
marketsListUMStateManager.onUiItemsChanged(
uiItems = marketsItemsData.items,
isInErrorState = marketsItemsData.isInErrorState,
isSearchNotFound = marketsItemsData.isSearchNotFound,
marketsNotificationUM = if (shouldShowYieldModePromo) {
MarketsNotificationUM.YieldSupplyPromo(
onClick = {
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModeMoreInfoClicked())
marketsListUMStateManager.selectedSortByType = SortByTypeUM.YieldSupply
},
onCloseClick = { onYieldModeNotificationCloseClick() },
)
} else {
null
},
)
}
}
state.onEach { marketsListUM ->
if (marketsListUM.list !is ListUM.Content) {
visibleItemIds.value = emptyList()
}
}.launchIn(modelScope)
// update all lists when user's currency has changed
currentAppCurrency.drop(1).onEach {
mainMarketsListManager.reload()
if (marketsListUMStateManager.isInSearchState) {
searchMarketsListManager.reload()
}
}.launchIn(modelScope)
// load charts when new batch is being loaded
mainMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey ->
mainMarketsListManager.loadCharts(setOf(batchKey), marketsListUMStateManager.selectedInterval)
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
}.launchIn(modelScope)
// listen currently selected interval, update charts if sorting=rating, or reload all list
modelScope.launch(dispatchers.default) {
marketsListUMStateManager.state.map { it.selectedInterval }.distinctUntilChanged().drop(1)
.collectLatest { interval ->
when (marketsListUMStateManager.selectedSortByType) {
SortByTypeUM.Rating -> {
mainMarketsListManager.updateUIWithSameState()
val batchKeys = mainMarketsListManager.getBatchKeysByItemIds(visibleItemIds.value)
mainMarketsListManager.loadCharts(batchKeys, interval)
}
else -> mainMarketsListManager.reload()
}
}
}
// reload list when sorting type has changed
modelScope.launch {
marketsListUMStateManager
.state
.map { it.selectedSortBy }
.distinctUntilChanged()
.drop(1)
.collectLatest { _ ->
mainMarketsListManager.reload()
}
}
// listen current visible batch and update charts
modelScope.launch {
visibleItemIds.mapNotNull { listOfIds ->
if (listOfIds.isNotEmpty()) {
activeListManager.getBatchKeysByItemIds(visibleItemIds.value)
} else {
null
}
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
// TODO load batch on scroll heat area
activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval)
}
}
// ===Search===
modelScope.launch {
marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode ->
activeListManager = if (isInSearchMode) {
searchMarketsListManager
} else {
searchMarketsListManager.clearStateAndStopAllActions()
mainMarketsListManager
}
}
}
modelScope.launch {
marketsListUMStateManager.searchQueryFlow.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
.distinctUntilChanged().onEach {
if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions()
}.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }.collectLatest {
searchMarketsListManager.reload(searchText = it)
}
}
modelScope.launch {
searchMarketsListManager.onLastBatchLoadedSuccess.collectLatest { batchKey ->
searchMarketsListManager.loadCharts(setOf(batchKey), marketsListUMStateManager.selectedInterval)
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
}
}
searchMarketsListManager
.isSearchNotFoundState
.onEach { isTokenFound ->
if (isTokenFound) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(isTokenFound = false))
}
}.launchIn(modelScope)
searchMarketsListManager.onFirstBatchLoadedSuccess.onEach {
analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(isTokenFound = true))
}.launchIn(modelScope)
// analytics
initAnalytics()
// initial loading
mainMarketsListManager.reload()
}
private fun MarketsListUM.TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval {
return when (this) {
MarketsListUM.TrendInterval.H24 -> TokenMarketListConfig.Interval.H24
MarketsListUM.TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK
MarketsListUM.TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH
}
}
private fun initAnalytics() {
state.filter { it.isInSearchMode.not() }
.map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged()
.onEach {
analyticsEventHandler.send(it)
}.launchIn(modelScope)
}
private fun onTokenUIClicked(token: MarketsListItemUM) {
modelScope.launch {
activeListManager.getTokenById(token.id)?.let { found ->
params.onTokenClick(found.toSerializableParam(), currentAppCurrency.value)
}
}
}
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
launch {
while (true) {
delay(timeMillis)
isVisibleOnScreen.first { it }
activeListManager.updateQuotes()
}
}.saveIn(updateQuotesJob)
}
private fun onYieldModeNotificationCloseClick() {
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoClosed())
modelScope.launch {
promoRepository.setMarketsYieldSupplyNotificationHideClicked()
}
}
private class MarketsItemsData(
val items: ImmutableList<MarketsListItemUM>,
val isInErrorState: Boolean,
val isSearchNotFound: Boolean,
val shouldShowYieldModePromo: Boolean,
)
}

View file

@ -0,0 +1,49 @@
package com.tangem.features.feed.model.market.list.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
internal sealed class MarketsListAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Markets", event = event, params = params) {
data class SortBy(
val sortByTypeUM: SortByTypeUM,
val interval: MarketsListUM.TrendInterval,
) : MarketsListAnalyticsEvent(
event = "Sort By",
params = mapOf(
"Type" to when (sortByTypeUM) {
SortByTypeUM.Rating -> "Rating"
SortByTypeUM.Trending -> "Trending"
SortByTypeUM.ExperiencedBuyers -> "Buyers"
SortByTypeUM.TopGainers -> "Gainers"
SortByTypeUM.TopLosers -> "Losers"
SortByTypeUM.Staking -> "Staking"
SortByTypeUM.YieldSupply -> "Yield Supply"
},
"Period" to when (interval) {
MarketsListUM.TrendInterval.H24 -> "24h"
MarketsListUM.TrendInterval.D7 -> "7d"
MarketsListUM.TrendInterval.M1 -> "1m"
},
),
)
class YieldModePromoShown : MarketsListAnalyticsEvent(event = "Notice - Yield Mode Promo")
class YieldModePromoClosed : MarketsListAnalyticsEvent(event = "Yield Mode Promo Closed")
class YieldModeMoreInfoClicked : MarketsListAnalyticsEvent(event = "Yield Mode More Info")
data class TokenSearched(val isTokenFound: Boolean) : MarketsListAnalyticsEvent(
event = "Token Searched",
params = mapOf(
"Result" to if (isTokenFound) "Yes" else "No",
),
)
class ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens")
}

View file

@ -0,0 +1,372 @@
package com.tangem.features.feed.model.market.list.statemanager
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
import com.tangem.features.feed.model.market.list.utils.logAction
import com.tangem.features.feed.model.market.list.utils.logStatus
import com.tangem.features.feed.model.market.list.utils.logUpdateResults
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
private const val LOG_EVENTS = true
@Suppress("LongParameterList", "LargeClass")
internal class MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
private val currentTrendInterval: Provider<MarketsListUM.TrendInterval>,
private val currentAppCurrency: Provider<AppCurrency>,
private val currentSearchText: Provider<String?>,
private val currentSortByType: Provider<SortByTypeUM>,
private val modelScope: CoroutineScope,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
private val updateStateJob = JobHolder()
private val batchFlow = getMarketsTokenListFlowUseCase(
batchingContext = TokenListBatchingContext(
actionsFlow = actionsFlow,
coroutineScope = modelScope,
),
batchFlowType = batchFlowType,
)
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = resultBatches.map { it.uiBatches }
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
get() = uiBatches
.map { batches ->
batches.asSequence()
.map { it.data }
.flatten()
.toImmutableList()
}
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = persistentListOf(),
)
val onLastBatchLoadedSuccess = batchFlow.state
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
.mapNotNull { batchListState ->
when (val status = batchListState.status) {
is PaginationStatus.Paginating -> {
if (status.lastResult is BatchFetchResult.Success) {
batchListState.data.lastOrNull()?.key
} else {
null
}
}
is PaginationStatus.EndOfPagination -> {
batchListState.data.lastOrNull()?.key
}
else -> null
}
}
val onFirstBatchLoadedSuccess = batchFlow.state
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
.mapNotNull { batchListState ->
when (val status = batchListState.status) {
is PaginationStatus.Paginating -> {
if (status.lastResult is BatchFetchResult.Success) {
batchListState.data.size == 1
} else {
null
}
}
is PaginationStatus.EndOfPagination -> {
batchListState.data.size == 1
}
else -> null
}
}
.filter { it }
val isInInitialLoadingErrorState = batchFlow.state
.map { it.status is PaginationStatus.InitialLoadingError }
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
val isSearchNotFoundState = batchFlow.state
.map { batchListState ->
currentSearchText().isNullOrEmpty().not() &&
batchListState.status is PaginationStatus.EndOfPagination &&
batchListState.data.isEmpty()
}
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
init {
batchFlow.state
.map { it.data }
.distinctUntilChanged { a, b ->
a.size == b.size &&
a.map { it.key } == b.map { it.key } &&
a.map { it.data }.flatten() == b.map { it.data }.flatten()
}
.onEach {
coroutineScope {
launch {
updateState(it)
}.saveIn(updateStateJob)
}
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
if (LOG_EVENTS) {
batchFlow.updateResults
.onEach { logUpdateResults(batchFlowType.name, it) }
.launchIn(modelScope)
batchFlow.state
.map { it.status }
.onEach { logStatus(batchFlowType.name, it) }
.launchIn(modelScope)
actionsFlow
.onEach { logAction(batchFlowType.name, it) }
.launchIn(modelScope)
}
}
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
withContext(dispatchers.default) {
resultBatches.update { resultBatches ->
val items = resultBatches.uiBatches
val previousList = resultBatches.processedItems
val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency())
if (newList.isEmpty()) {
return@update ResultBatches(processedItems = emptyList())
}
val isInitialLoading =
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
val outItems = if (isInitialLoading) {
newList.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
if (previousList.size != newList.size) {
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
val newBatches = newList.filter { keysToAdd.contains(it.key) }
items + newBatches.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
items.mapIndexed { batchIndex, batch ->
val prevBatch = previousList[batchIndex]
val newBatch = newList[batchIndex]
if (previousList == newBatch) return@mapIndexed batch
Batch(
key = batch.key,
data = batch.data.mapIndexed { index, marketsListItemUM ->
val prevItem = prevBatch.data[index]
val newItem = newBatch.data[index]
converter.update(
prevItem,
marketsListItemUM,
newItem,
)
},
)
}
}
}
currentCoroutineContext().ensureActive()
ResultBatches(
uiBatches = outItems,
processedItems = newList,
)
}
}
fun reload(searchText: String? = null) {
modelScope.launch {
resultBatches.value = ResultBatches()
actionsFlow.emit(
BatchAction.Reload(
requestParams = TokenMarketListConfig(
fiatPriceCurrency = currentAppCurrency().code,
searchText = if (currentSearchText() == null) {
null
} else {
searchText ?: currentSearchText()
},
priceChangeInterval = currentTrendInterval().toBatchRequestInterval(),
order = currentSortByType().toRequestOrder(),
),
),
)
}
}
fun loadMore() {
modelScope.launch {
actionsFlow.emit(BatchAction.LoadMore())
}
}
fun updateUIWithSameState() {
modelScope.launch(dispatchers.default) {
val current = batchFlow.state.value.data
updateState(current, forceUpdate = true)
}.saveIn(updateStateJob)
}
fun loadCharts(batchKeys: Set<Int>, interval: MarketsListUM.TrendInterval) {
if (batchKeys.isEmpty()) return
modelScope.launch {
val currentData = batchFlow.state.value.data
val alreadyLoadedChartsBatchKeys = currentData
.filter { batch ->
val first = batch.data.firstOrNull() ?: return@filter false
val chartByInterval = when (interval) {
MarketsListUM.TrendInterval.H24 -> first.tokenCharts.h24
MarketsListUM.TrendInterval.D7 -> first.tokenCharts.week
MarketsListUM.TrendInterval.M1 -> first.tokenCharts.month
}
chartByInterval != null
}
.map { it.key }
.toSet()
val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys)
if (batchesKeysToLoad.isNotEmpty()) {
actionsFlow.emit(
BatchAction.UpdateBatches(
keys = batchesKeysToLoad,
updateRequest = TokenMarketUpdateRequest.UpdateChart(
interval = interval.toBatchRequestInterval(),
currency = currentAppCurrency().code,
),
async = true,
operationId = batchesKeysToLoad.toString() + interval.toString(),
),
)
}
}
}
fun updateQuotes() {
modelScope.launch {
actionsFlow.emit(
BatchAction.CancelUpdates {
it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes
},
)
actionsFlow.emit(
BatchAction.UpdateBatches(
keys = batchFlow
.state
.value
.data
.map { it.key }
.toSet(),
updateRequest = TokenMarketUpdateRequest.UpdateQuotes(
currencyId = currentAppCurrency().code,
),
async = true,
operationId = "update quotes",
),
)
}
}
fun clearStateAndStopAllActions() {
resultBatches.value = ResultBatches()
modelScope.launch {
actionsFlow.emit(BatchAction.Reset)
}
}
fun getBatchKeysByItemIds(ids: List<CryptoCurrency.RawID>): Set<Int> {
val currentData = batchFlow.state.value.data
return currentData
.filter { d -> d.data.any { ids.contains(it.id) } }
.map { it.key }
.toSet()
}
fun getTokenById(id: CryptoCurrency.RawID): TokenMarket? {
return batchFlow
.state
.value
.data
.map { it.data }
.flatten()
.find { it.id == id }
}
private fun SortByTypeUM.toRequestOrder(): TokenMarketListConfig.Order {
return when (this) {
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply
}
}
private fun MarketsListUM.TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval {
return when (this) {
MarketsListUM.TrendInterval.H24 -> TokenMarketListConfig.Interval.H24
MarketsListUM.TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK
MarketsListUM.TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH
}
}
private data class ResultBatches(
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
)
}

View file

@ -0,0 +1,267 @@
package com.tangem.features.feed.model.market.list.statemanager
import androidx.compose.runtime.Stable
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.list.state.*
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
@Stable
@Suppress("LongParameterList", "LargeClass")
internal class MarketsListUMStateManager(
private val shouldAlwaysShowSearchBar: Provider<Boolean>,
private val currentVisibleIds: Provider<List<CryptoCurrency.RawID>>,
private val preselectedSortType: Provider<SortByTypeUM>,
private val onLoadMoreUiItems: () -> Unit,
private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> Unit,
private val onRetryButtonClicked: () -> Unit,
private val onTokenClick: (MarketsListItemUM) -> Unit,
private val onShowTokensUnder100kClicked: () -> Unit,
) {
val state = MutableStateFlow(state())
private var isSortByBottomSheetShown
get() = state.value.sortByBottomSheet.isShown
set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShown = value)) }
var searchQuery
get() = state.value.searchBar.query
private set(value) = state.update { marketsListUM ->
marketsListUM.copy(
searchBar = marketsListUM.searchBar.copy(
query = value,
isActive = value.isNotEmpty(),
),
)
}
var isInSearchState
get() = state.value.searchBar.isActive
private set(value) = state.update { it.copy(searchBar = it.searchBar.copy(isActive = value)) }
var selectedSortByType
get() = state.value.selectedSortBy
set(value) = state.update { marketsListUM ->
marketsListUM.copy(
selectedSortBy = value,
sortByBottomSheet = marketsListUM.sortByBottomSheet.copy(
content = (marketsListUM.sortByBottomSheet.content as SortByBottomSheetContentUM).copy(
selectedOption = value,
),
),
list = if (marketsListUM.list is ListUM.Content && marketsListUM.selectedSortBy != value) {
marketsListUM.list.copy(
triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() },
)
} else {
marketsListUM.list
},
)
}
var selectedInterval
get() = state.value.selectedInterval
set(value) = state.update { marketsListUM ->
marketsListUM.copy(
selectedInterval = value,
list = if (marketsListUM.list is ListUM.Content &&
marketsListUM.selectedSortBy != SortByTypeUM.Rating &&
marketsListUM.selectedInterval != value
) {
marketsListUM.list.copy(
triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() },
)
} else {
marketsListUM.list
},
)
}
val isInSearchStateFlow = state.map { it.isInSearchMode }.distinctUntilChanged()
val searchQueryFlow = state.map { it.searchBar.query }.distinctUntilChanged()
fun onUiItemsChanged(
isInErrorState: Boolean,
isSearchNotFound: Boolean,
uiItems: ImmutableList<MarketsListItemUM>,
marketsNotificationUM: MarketsNotificationUM?,
) {
state.update { marketsListUM ->
when {
isInErrorState -> {
marketsListUM.copy(
list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked),
)
}
isSearchNotFound -> {
marketsListUM.copy(list = ListUM.SearchNothingFound)
}
uiItems.isEmpty() -> {
marketsListUM.copy(list = ListUM.Loading)
}
else -> {
marketsListUM.updateItems(
newItems = uiItems,
marketsNotificationUM = marketsNotificationUM,
)
}
}
}
}
private fun MarketsListUM.updateItems(
newItems: ImmutableList<MarketsListItemUM>,
marketsNotificationUM: MarketsNotificationUM?,
): MarketsListUM {
val currentState = this
if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) {
val itemsWithFilteredPriceChange = newItems.filterPriceChangeByVisibility()
return currentState.copy(
list = generalContentState(itemsWithFilteredPriceChange)
.copy(
shouldShowUnder100kTokensNotificationWasHidden = currentState
.showUnder100kButtonAlreadyPressed(),
),
marketsNotificationUM = marketsNotificationUM,
)
}
// Search state cases
val filtered = newItems.filter { item -> item.isUnder100kMarketCap.not() }
.toImmutableList()
.filterPriceChangeByVisibility()
if (filtered.size != newItems.size) {
val searchUiItemsCached = newItems.filterPriceChangeByVisibility()
return currentState.copy(
list = generalContentState(filtered).copy(
shouldShowUnder100kTokensNotificationWasHidden = false,
shouldShowUnder100kTokensNotification = true,
onShowTokensUnder100kClicked = {
onShowTokensUnder100kClicked()
state.update { s ->
(s.list as? ListUM.Content)?.let {
s.copy(
list = s.list.copy(
items = searchUiItemsCached,
shouldShowUnder100kTokensNotification = false,
shouldShowUnder100kTokensNotificationWasHidden = true,
),
)
} ?: s
}
},
),
)
} else {
return currentState.copy(
list = generalContentState(newItems.filterPriceChangeByVisibility()),
)
}
}
private fun MarketsListUM.showUnder100kButtonAlreadyPressed(): Boolean {
return this.list is ListUM.Content &&
this.isInSearchMode &&
this.list.shouldShowUnder100kTokensNotificationWasHidden
}
// Show price change animation for visible items only
private fun ImmutableList<MarketsListItemUM>.filterPriceChangeByVisibility(): ImmutableList<MarketsListItemUM> {
val visibleItemIds = currentVisibleIds()
return map { marketsListItemUM ->
marketsListItemUM.copy(
price = marketsListItemUM.price.copy(
changeType = if (visibleItemIds.contains(marketsListItemUM.id)) {
marketsListItemUM.price.changeType
} else {
null
},
),
)
}.toImmutableList()
}
private fun generalContentState(newItems: ImmutableList<MarketsListItemUM>): ListUM.Content {
return ListUM.Content(
items = newItems,
loadMore = onLoadMoreUiItems,
visibleIdsChanged = visibleItemsChanged,
shouldShowUnder100kTokensNotification = false,
onShowTokensUnder100kClicked = {},
triggerScrollReset = consumedEvent(),
onItemClick = onTokenClick,
shouldShowUnder100kTokensNotificationWasHidden = false,
)
}
private fun state(): MarketsListUM = MarketsListUM(
list = ListUM.Loading,
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.markets_search_header_title),
query = "",
onQueryChange = { searchQuery = it },
isActive = false,
onActiveChange = { },
),
selectedSortBy = preselectedSortType(),
selectedInterval = MarketsListUM.TrendInterval.H24,
onIntervalClick = { selectedInterval = it },
onSortByButtonClick = { isSortByBottomSheetShown = true },
sortByBottomSheet = TangemBottomSheetConfig(
isShown = false,
onDismissRequest = { isSortByBottomSheetShown = false },
content = SortByBottomSheetContentUM(
selectedOption = preselectedSortType(),
onOptionClicked = ::onBottomSheetOptionClicked,
),
),
marketsNotificationUM = null,
shouldAlwaysShowSearchBar = shouldAlwaysShowSearchBar(),
)
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {
state.update { marketsListUM ->
marketsListUM.copy(
selectedSortBy = sortByTypeUM,
sortByBottomSheet = marketsListUM.sortByBottomSheet.copy(
isShown = false,
content = (marketsListUM.sortByBottomSheet.content as SortByBottomSheetContentUM).copy(
selectedOption = sortByTypeUM,
),
),
)
}
}
private fun consumeTriggerResetScrollEvent() {
state.update { marketsListUM ->
marketsListUM.copy(
list = if (marketsListUM.list is ListUM.Content) {
marketsListUM.list.copy(
triggerScrollReset = consumedEvent(),
)
} else {
marketsListUM.list
},
)
}
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.features.feed.model.market.list.utils
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListConfig
import com.tangem.domain.markets.TokenMarketUpdateRequest
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchUpdateResult
import com.tangem.pagination.PaginationStatus
import timber.log.Timber
internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>) {
Timber.tag(tag).d(
"""
Status
$status
""".trimIndent(),
)
}
internal fun logAction(tag: String, action: BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>) {
when (action) {
is BatchAction.Reload -> Timber.tag(tag).d(
"""
Reload = ${action.requestParams}
""".trimIndent(),
)
is BatchAction.UpdateBatches -> Timber.tag(tag).d(
"""
To update:
keys: ${action.keys.toList()}
updateType: ${action.updateRequest.javaClass.simpleName}
""".trimIndent(),
)
else -> Timber.tag(tag).d(
"""
$action
""".trimIndent(),
)
}
}
internal fun logUpdateResults(
tag: String,
updateResult: Pair<TokenMarketUpdateRequest, BatchUpdateResult<Int, List<TokenMarket>>>,
) {
val sec = when (val s = updateResult.second) {
is BatchUpdateResult.Success -> "Success"
is BatchUpdateResult.Error -> s.throwable.toString()
}
Timber.tag(tag).d(
"""
updateResults
request: ${updateResult.first}
result: $sec
""".trimIndent(),
)
}

View file

@ -1,6 +1,9 @@
package com.tangem.features.feed.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
@ -12,6 +15,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.features.feed.components.FeedEntryChildFactory
@Composable
@ -20,8 +24,10 @@ internal fun EntryBottomSheetContent(
onHeaderSizeChange: (Dp) -> Unit,
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
Scaffold(
containerColor = background,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
AnimatedContent(
@ -33,13 +39,15 @@ internal fun EntryBottomSheetContent(
}
}
},
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { currentState ->
currentState.Title()
}
},
content = { contentPadding ->
AnimatedContent(
stackState.active.instance,
targetState = stackState.active.instance,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { currentState ->
currentState.Content(modifier = Modifier.padding(contentPadding))
}

View file

@ -52,7 +52,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
@Composable
internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
@ -269,6 +269,7 @@ private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendi
}
}
@Suppress("LongMethod")
@Composable
private fun NewsContentBlock(
feedListCallbacks: FeedListCallbacks,
@ -315,21 +316,23 @@ private fun NewsContentBlock(
)
SpacerH(12.dp)
trendingArticle?.let { article ->
ArticleCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
articleConfigUM = article,
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
SpacerH(12.dp)
if (trendingArticle != null) {
Column {
ArticleCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
articleConfigUM = trendingArticle,
onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) },
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
SpacerH(12.dp)
}
}
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
@ -347,6 +350,7 @@ private fun NewsContentBlock(
)
}
}
SpacerH(32.dp)
}
}
@ -388,10 +392,16 @@ private fun Charts(
}
}
is MarketChartUM.LoadingError -> {
UnableToLoadData(
onRetryClick = marketChart.onRetryClicked,
modifier = Modifier.fillMaxWidth(),
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 35.dp, horizontal = 10.dp),
) {
UnableToLoadData(
onRetryClick = marketChart.onRetryClicked,
modifier = Modifier.fillMaxWidth(),
)
}
}
is MarketChartUM.Content -> {
marketChart.items.fastForEach { chart ->
@ -468,10 +478,18 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
onSeeAllClick = {},
)
SpacerH(12.dp)
UnableToLoadData(
onRetryClick = onRetryClick,
modifier = Modifier.fillMaxWidth(),
)
BlockCard(
modifier = Modifier.padding(horizontal = 16.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
UnableToLoadData(
onRetryClick = onRetryClick,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 35.dp, horizontal = 10.dp),
)
}
}
}
@ -481,7 +499,7 @@ private const val GRADIENT_END = 0.5f
private val LinearGradientFirstPart = Color(0xFF635EEC)
private val LinearGradientSecondPart = Color(0xFFE05AED)
@Preview(showBackground = true)
@Preview(showBackground = true, heightDp = 1500)
@Composable
private fun FeedListPreview() {
TangemThemePreview {

View file

@ -11,7 +11,7 @@ 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.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import kotlinx.collections.immutable.*
@Suppress("MagicNumber")

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toPersistentList

View file

@ -5,8 +5,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
import com.tangem.features.feed.ui.market.state.MarketsListUM
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.features.feed.ui.market.list.state.MarketsListUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchFetchResult

View file

@ -0,0 +1,350 @@
package com.tangem.features.feed.ui.market.list
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
import com.tangem.features.feed.ui.market.list.components.YieldSupplyInMarketsPromoNotification
import com.tangem.features.feed.ui.market.list.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
private const val SHOW_MORE_KEY = "privacyPolicy"
@Composable
internal fun TopBarWithSearch(searchBarUM: SearchBarUM) {
val background = LocalMainBottomSheetColor.current.value
SearchBar(
modifier = Modifier
.drawBehind { drawRect(background) }
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp),
state = searchBarUM,
colors = TangemSearchBarDefaults.defaultTextFieldColors.copy(
focusedContainerColor = TangemTheme.colors.field.focused,
unfocusedContainerColor = TangemTheme.colors.field.focused,
),
)
}
@Composable
internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.imePadding()
.drawBehind { drawRect(background) },
) {
Content(state = state)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown)
}
@Suppress("LongMethod")
@Composable
private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) {
val strokeColor = TangemTheme.colors.stroke.primary
val scrolledState = remember { mutableStateOf(false) }
Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) {
AnimatedVisibility(
visible = scrolledState.value.not(),
) {
Column {
SpacerH8()
Title(isInSearchMode = state.isInSearchMode)
SpacerH12()
}
}
Column {
AnimatedVisibility(state.isInSearchMode.not()) {
Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
)
}
val marketsNotification = state.marketsNotificationUM
AnimatedVisibility(
state.isInSearchMode.not() &&
state.selectedSortBy != SortByTypeUM.YieldSupply,
) {
val showMore = stringResourceSafe(R.string.common_show_more)
when (marketsNotification) {
is MarketsNotificationUM.YieldSupplyPromo -> {
val description = stringResourceSafe(
R.string.markets_yield_supply_banner_description,
showMore,
)
val clickableDescription = annotatedReference {
append(description.substringBefore(showMore))
pushStringAnnotation(SHOW_MORE_KEY, "")
appendColored(showMore, TangemTheme.colors.text.accent)
pop()
}
YieldSupplyInMarketsPromoNotification(
config = marketsNotification.config.copy(
subtitle = clickableDescription,
),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
else -> { /* no-op */
}
}
}
}
}
val strokeWidth = TangemTheme.dimens.size0_5
Box(
Modifier
.fillMaxWidth()
.height(strokeWidth)
.drawBehind {
// draw horizontal line
if (scrolledState.value) {
drawLine(
color = strokeColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth.toPx(),
)
}
},
)
ItemsList(
scrolledState = scrolledState,
isInSearchMode = state.isInSearchMode,
state = state.list,
)
}
@Composable
private fun Title(isInSearchMode: Boolean, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = if (isInSearchMode) {
stringResourceSafe(id = R.string.markets_search_result_title)
} else {
stringResourceSafe(id = R.string.markets_common_title)
},
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
@Composable
private fun Options(
sortByTypeUM: SortByTypeUM,
trendInterval: MarketsListUM.TrendInterval,
onSortByClick: () -> Unit,
onIntervalClick: (MarketsListUM.TrendInterval) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(IntrinsicSize.Max)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = sortByTypeUM.text,
onClick = onSortByClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
),
)
SegmentedButtons(
config = persistentListOf(
MarketsListUM.TrendInterval.H24,
MarketsListUM.TrendInterval.D7,
MarketsListUM.TrendInterval.M1,
),
color = TangemTheme.colors.button.secondary,
initialSelectedItem = trendInterval,
onClick = onIntervalClick,
modifier = Modifier
.width(160.dp)
.fillMaxHeight(),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(
vertical = TangemTheme.dimens.spacing4,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = it.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Composable
private fun ItemsList(
scrolledState: MutableState<Boolean>,
isInSearchMode: Boolean,
state: ListUM,
modifier: Modifier = Modifier,
) {
val searchLazyListState = rememberLazyListState()
val mainLazyListState = rememberLazyListState()
val isMainScrolled by remember {
derivedStateOf {
mainLazyListState.firstVisibleItemScrollOffset > 0
}
}
val isSearchScrolled by remember {
derivedStateOf {
searchLazyListState.firstVisibleItemScrollOffset > 0
}
}
LaunchedEffect(isMainScrolled, isInSearchMode, isSearchScrolled) {
scrolledState.value = if (isInSearchMode) {
isSearchScrolled
} else {
isMainScrolled
}
}
MarketsListLazyColumn(
modifier = modifier,
state = state,
isInSearchMode = isInSearchMode,
lazyListState = if (isInSearchMode) {
searchLazyListState
} else {
mainLazyListState
},
)
}
@Composable
private fun KeyboardEvents(isSortByBottomSheetShown: Boolean) {
val keyboardController = LocalSoftwareKeyboardController.current
val keyboard by keyboardAsState()
val focusManager = LocalFocusManager.current
BackHandler(enabled = keyboard is Keyboard.Opened) {
keyboardController?.hide()
}
LaunchedEffect(keyboard) {
if (keyboard is Keyboard.Closed) {
focusManager.clearFocus()
}
}
LaunchedEffect(isSortByBottomSheetShown) {
keyboardController?.hide()
}
}
//region: Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(alwaysShowBottomSheets = false) {
val primaryBackground = TangemTheme.colors.background.primary
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) },
) {
MarketsList(
state = MarketsListUM(
list = ListUM.Content(
items = MarketChartListItemPreviewDataProvider().values
.flatMap { item -> List(size = 10) { item } }
.mapIndexed { index, item ->
item.copy(id = CryptoCurrency.RawID(index.toString()))
}
.toImmutableList(),
shouldShowUnder100kTokensNotification = false,
shouldShowUnder100kTokensNotificationWasHidden = false,
loadMore = {},
visibleIdsChanged = {},
onShowTokensUnder100kClicked = {},
triggerScrollReset = consumedEvent(),
onItemClick = {},
),
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.markets_search_header_title),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
),
selectedSortBy = SortByTypeUM.Rating,
selectedInterval = MarketsListUM.TrendInterval.H24,
onIntervalClick = {},
onSortByButtonClick = {},
sortByBottomSheet = TangemBottomSheetConfig(
isShown = false,
onDismissRequest = {},
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
),
marketsNotificationUM = MarketsNotificationUM.YieldSupplyPromo(
onClick = {},
onCloseClick = {},
),
shouldAlwaysShowSearchBar = true,
),
)
}
}
}
//endregion: Preview

View file

@ -0,0 +1,214 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import com.tangem.common.ui.markets.MarketsListItem
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.list.state.ListUM
import kotlinx.coroutines.launch
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
private const val LOAD_NEXT_PAGE_ON_END_INDEX_SEARCH = 25
@Composable
@Suppress("LongMethod")
internal fun MarketsListLazyColumn(
state: ListUM,
isInSearchMode: Boolean,
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val coroutineScope = rememberCoroutineScope()
SideEffect {
if (state is ListUM.Loading) {
coroutineScope.launch {
lazyListState.scrollToItem(0)
}
}
}
if (state is ListUM.Content) {
EventEffect(state.triggerScrollReset) {
lazyListState.scrollToItem(0)
}
}
if (state is ListUM.Loading) {
LazyColumn(
modifier = modifier,
state = rememberLazyListState(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = false,
) {
items(count = 100, key = { it }) {
MarketsListItemPlaceholder()
}
}
} else {
LazyColumn(
modifier = modifier.testTag(MarketsTestTags.TOKENS_LIST),
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = true,
) {
// ATTENTION! There should be no elements with a string key value except MarketsListItem!
when (state) {
is ListUM.LoadingError -> {
item(key = "loading error".hashCode()) {
LoadingErrorItem(
modifier = Modifier.fillParentMaxSize(),
onTryAgain = state.onRetryClicked,
)
}
}
ListUM.SearchNothingFound -> {
item(key = "not found text".hashCode()) {
SearchNothingFoundText(
modifier = Modifier.fillParentMaxSize(),
)
}
}
is ListUM.Content -> {
items(
items = state.items,
key = { it.getComposeKey() },
) { item ->
MarketsListItem(
model = item,
onClick = { state.onItemClick(item) },
)
}
if (isInSearchMode && state.shouldShowUnder100kTokensNotification) {
item(key = "show tokens under 100k".hashCode()) {
ShowTokensUnder100kItem(
onShowTokensClick = state.onShowTokensUnder100kClicked,
)
}
}
}
else -> {}
}
}
}
VisibleItemsTracker(lazyListState, state)
InfiniteListHandler(
listState = lazyListState,
buffer = if (isInSearchMode) {
LOAD_NEXT_PAGE_ON_END_INDEX_SEARCH
} else {
LOAD_NEXT_PAGE_ON_END_INDEX
},
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(state) {
{
if (state is ListUM.Content && state.shouldShowUnder100kTokensNotification.not()) {
state.loadMore()
true
} else {
false
}
}
},
)
}
@Composable
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = onTryAgain)
}
}
@Composable
private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.markets_search_show_tokens),
onClick = onShowTokensClick,
),
)
}
}
@Composable
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(R.string.markets_search_token_no_result_title),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
val visibleItems by remember {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.mapNotNull { lazyListItemInfo ->
(lazyListItemInfo.key as? String)
?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)
?.first()
?.let { rawId -> CryptoCurrency.RawID(rawId) }
}
}
}
LaunchedEffect(listState.isScrollInProgress, visibleItems) {
if (state is ListUM.Content && listState.isScrollInProgress.not()) {
state.visibleIdsChanged(visibleItems)
}
}
}

View file

@ -0,0 +1,85 @@
package com.tangem.features.feed.ui.market.list.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.list.state.SortByBottomSheetContentUM
import com.tangem.features.feed.ui.market.list.state.SortByTypeUM
@Composable
fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<SortByBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.markets_sort_by_title),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: SortByBottomSheetContentUM) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
SortByTypeUM.entries.forEachIndexed { index, type ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = SortByTypeUM.entries.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClicked(type) },
showDivider = index != SortByTypeUM.entries.lastIndex,
) {
InputRowChecked(
text = type.text,
checked = type == content.selectedOption,
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
MarketsListSortByBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = SortByBottomSheetContentUM(
selectedOption = SortByTypeUM.Trending,
onOptionClicked = {},
),
),
)
}
}
}

View file

@ -0,0 +1,157 @@
package com.tangem.features.feed.ui.market.list.components
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.notifications.CloseableIconButton
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private val bgColor = Color(0x1F8CD9FF)
private val borderColor = Color(0x3D8CD9FF)
@Composable
fun StakingInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
var textHeightDp by remember { mutableStateOf(0.dp) }
Box(
modifier = modifier
.fillMaxWidth()
.border(
width = 1.dp,
shape = TangemTheme.shapes.roundedCornersXMedium,
color = borderColor,
)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(bgColor)
.clickable { config.onClick?.invoke() },
) {
PromoImage(
iconRes = config.iconResId,
modifier = Modifier.height(textHeightDp),
)
PromoText(
title = config.title,
subtitle = config.subtitle,
onSizeChange = { textHeightDp = it },
)
CloseableIconButton(
onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
iconTint = TangemTheme.colors.icon.secondary,
)
}
}
@Composable
private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) {
Box(
modifier = modifier.padding(12.dp),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.requiredWidth(56.dp)
.wrapContentHeight(Alignment.Top, unbounded = true),
)
}
}
@Composable
private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) {
val density = LocalDensity.current
Box(
modifier = Modifier.onSizeChanged {
with(density) { onSizeChange(it.height.toDp()) }
},
) {
TextsBlock(
title = title,
subtitle = subtitle,
modifier = Modifier
.wrapContentHeight()
.align(Alignment.CenterStart)
.padding(start = 76.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
)
}
}
@Composable
private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
val titleText = title?.resolveReference()
if (titleText != null) {
Text(
text = titleText,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
)
SpacerH(height = TangemTheme.dimens.spacing2)
}
Text(
text = subtitle.resolveAnnotatedReference(),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.caption2,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun RingPromoNotification_Preview() {
TangemThemePreview {
StakingInMarketsPromoNotification(
config = NotificationConfig(
title = stringReference("Earn up to 14% APY"),
subtitle = stringReference("Staking is the easiest way to earn rewards on your crypto. Show more"),
iconResId = R.drawable.img_staking_in_market_notification,
onCloseClick = { },
),
)
}
}
// endregion

View file

@ -0,0 +1,157 @@
package com.tangem.features.feed.ui.market.list.components
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.notifications.CloseableIconButton
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private val bgColor = Color(0x2684B0D7)
private val borderColor = Color(0x2684B0D7)
@Composable
fun YieldSupplyInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
var textHeightDp by remember { mutableStateOf(0.dp) }
Box(
modifier = modifier
.fillMaxWidth()
.border(
width = 1.dp,
shape = TangemTheme.shapes.roundedCornersXMedium,
color = borderColor,
)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(bgColor)
.clickable { config.onClick?.invoke() },
) {
PromoImage(
iconRes = config.iconResId,
modifier = Modifier.height(textHeightDp),
)
PromoText(
title = config.title,
subtitle = config.subtitle,
onSizeChange = { textHeightDp = it },
)
CloseableIconButton(
onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
iconTint = TangemTheme.colors.icon.secondary,
)
}
}
@Composable
private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) {
Box(
modifier = modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.requiredWidth(80.dp)
.wrapContentHeight(Alignment.CenterVertically, unbounded = true),
)
}
}
@Composable
private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) {
val density = LocalDensity.current
Box(
modifier = Modifier.onSizeChanged {
with(density) { onSizeChange(it.height.toDp()) }
},
) {
TextsBlock(
title = title,
subtitle = subtitle,
modifier = Modifier
.wrapContentHeight()
.align(Alignment.CenterStart)
.padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
)
}
}
@Composable
private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
val titleText = title?.resolveReference()
if (titleText != null) {
Text(
text = titleText,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
)
SpacerH(height = TangemTheme.dimens.spacing2)
}
Text(
text = subtitle.resolveAnnotatedReference(),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.caption2,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun RingPromoNotification_Preview() {
TangemThemePreview {
YieldSupplyInMarketsPromoNotification(
config = NotificationConfig(
title = stringReference("Activate Yield Mode"),
subtitle = stringReference("Power up your assets while supplying them with instant access. Show more"),
iconResId = R.drawable.img_yield_supply_in_market_notification,
onCloseClick = { },
),
)
}
}
// endregion

View file

@ -1,4 +1,4 @@
package com.tangem.features.feed.ui.market.state
package com.tangem.features.feed.ui.market.list.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM
@ -10,7 +10,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
internal data class MarketsListUM(
val list: ListUM,
@ -18,11 +17,10 @@ internal data class MarketsListUM(
val selectedSortBy: SortByTypeUM,
val sortByBottomSheet: TangemBottomSheetConfig,
val selectedInterval: TrendInterval,
val shouldAlwaysShowSearchBar: Boolean,
val onIntervalClick: (TrendInterval) -> Unit,
val onSortByButtonClick: () -> Unit,
val stakingNotificationMaxApy: BigDecimal?,
val onStakingNotificationClick: () -> Unit,
val onStakingNotificationCloseClick: () -> Unit,
val marketsNotificationUM: MarketsNotificationUM?,
) {
val isInSearchMode
get() = searchBar.isActive

View file

@ -0,0 +1,22 @@
package com.tangem.features.feed.ui.market.list.state
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.feed.impl.R
internal sealed class MarketsNotificationUM(val config: NotificationConfig) {
data class YieldSupplyPromo(
val onClick: () -> Unit,
val onCloseClick: () -> Unit,
) : MarketsNotificationUM(
config = NotificationConfig(
iconResId = R.drawable.img_yield_supply_in_market_notification,
title = resourceReference(R.string.markets_yield_supply_banner_title),
subtitle = TextReference.EMPTY,
onClick = onClick,
onCloseClick = onCloseClick,
),
)
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.feed.ui.market.list.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
data class SortByBottomSheetContentUM(
val selectedOption: SortByTypeUM,
val onOptionClicked: (SortByTypeUM) -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.markets)
implementation(projects.domain.onramp.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@ -23,7 +24,10 @@ 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.*
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
@ -248,10 +252,11 @@ private fun TokenPriceText(
color.animateTo(generalColor, tween(durationMillis = 500))
}
ResizableText(
Text(
text = price,
modifier = modifier,
color = color.value,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize),
maxLines = 1,
style = TangemTheme.typography.head,
)

View file

@ -174,7 +174,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
AppRoute.Staking(
userWalletId = cryptoCurrencyData.userWallet.walletId,
cryptoCurrency = cryptoCurrencyData.status.currency,
yieldId = option.integrationId,
integrationId = option.integrationId,
),
)
}

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@ -21,7 +22,10 @@ import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.containers.FooterContainer
@ -131,11 +135,14 @@ private fun AmountBlock(state: OnrampSuccessComponentUM.Content) {
error = { },
contentDescription = null,
)
ResizableText(
Text(
text = state.fromAmount.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
autoSize = TextAutoSize.StepBased(
maxFontSize = TangemTheme.typography.h2.fontSize,
),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()

View file

@ -29,6 +29,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.crypto
@ -122,7 +123,7 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie
}
}
TangemTooltip(
text = annotatedString,
text = annotatedReference(annotatedString),
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing6)
.size(TangemTheme.dimens.size16)

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -45,6 +46,7 @@ import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.v2.api.entity.*
import com.tangem.features.send.v2.api.params.FeeSelectorParams
@ -200,7 +202,9 @@ private fun CustomFeeBlock(
) {
Column(modifier = modifier) {
Row(
modifier = Modifier.padding(all = 12.dp),
modifier = Modifier
.padding(all = 12.dp)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@ -208,7 +212,8 @@ private fun CustomFeeBlock(
modifier = Modifier
.size(36.dp)
.background(color = iconBackgroundColor, shape = CircleShape)
.padding(6.dp),
.padding(6.dp)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON),
painter = painterResource(R.drawable.ic_edit_v2_24),
tint = iconTint,
contentDescription = null,
@ -217,6 +222,7 @@ private fun CustomFeeBlock(
text = stringResourceSafe(R.string.common_custom),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE),
)
}
AnimatedVisibility(
@ -289,7 +295,8 @@ private fun ExpandedCustomFeeItems(
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM),
)
}
}
@ -309,7 +316,9 @@ private fun RegularFeeItemContent(
) {
Column(modifier = modifier) {
Row(
modifier = Modifier.padding(all = 12.dp),
modifier = Modifier
.padding(all = 12.dp)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@ -317,7 +326,8 @@ private fun RegularFeeItemContent(
modifier = Modifier
.size(36.dp)
.background(color = iconBackgroundColor, shape = CircleShape)
.padding(6.dp),
.padding(6.dp)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON),
painter = painterResource(iconRes),
tint = iconTint,
contentDescription = null,
@ -352,6 +362,7 @@ private fun FeeDescription(
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE),
)
if (preDot != null) {
FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
@ -375,6 +386,7 @@ private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, elli
color = textColor,
textAlign = TextAlign.End,
ellipsis = ellipsis,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT),
)
if (postDot != null) {
Text(
@ -382,9 +394,16 @@ private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, elli
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.DOT_SIGN),
)
Text(
text = postDot.resolveReference(),
style = textStyle,
color = textColor,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT),
)
Text(text = postDot.resolveReference(), style = textStyle, color = textColor)
}
}
}

View file

@ -16,7 +16,7 @@ dependencies {
/** Domain models */
api(projects.domain.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.api
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
@ -10,7 +11,7 @@ interface StakingComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
val integrationId: StakingIntegrationID,
)
interface Factory : ComponentFactory<Params, StakingComponent>

View file

@ -18,10 +18,10 @@ internal class StakingAnalyticSender(
fun initialInfoScreen(value: StakingUiState) {
val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data
val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = validatorState?.balances
?.filterNot { it.validator?.address.isNullOrBlank() }
?.distinctBy { it.validator?.address }
val balanceState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val validatorCount = balanceState?.balances
?.filterNot { it.target?.address.isNullOrBlank() }
?.distinctBy { it.target?.address }
?.size ?: 0
analyticsEventHandler.send(
@ -34,7 +34,7 @@ internal class StakingAnalyticSender(
fun confirmationScreen(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
@ -80,7 +80,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
Basic.TransactionSent(
@ -102,7 +102,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenValidator?.name ?: return
val validatorName = validatorState?.chosenTarget?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonAction(

View file

@ -1,6 +1,5 @@
package com.tangem.features.staking.impl.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
@ -8,7 +7,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -28,7 +26,6 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
) : StakingDeepLinkHandler {
@ -73,19 +70,13 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
return@launch
}
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync(
val availability = getStakingAvailabilityUseCase.invokeSync(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
).getOrNull()
if (isStakingEnabled !is StakingAvailability.Available) {
return@launch
}
val yield = getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).getOrElse {
val option = (availability as? StakingAvailability.Available)?.option
if (option == null) {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
return@launch
}
@ -94,7 +85,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
AppRoute.Staking(
userWalletId = selectedUserWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = yield.id,
integrationId = option.integrationId,
),
)
}

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal
@ -35,7 +35,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openValidators()
fun onValidatorSelect(validator: Yield.Validator)
fun onTargetSelect(target: StakingTarget)
fun openRewardsValidators()

View file

@ -45,13 +45,12 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.*
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.utils.getValidatorsCount
import com.tangem.domain.tokens.*
import com.tangem.domain.transaction.error.GetFeeError
@ -135,6 +134,7 @@ internal class StakingModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
@ -165,9 +165,19 @@ internal class StakingModel @Inject constructor(
private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id
private val userWalletId: UserWalletId = params.userWalletId
private val yield: Yield = runBlocking {
getYieldUseCase(params.yieldId).getOrElse {
error("yield must be not null")
private val integration: StakingIntegration = runBlocking {
when (val integrationId = params.integrationId) {
is StakingIntegrationID.StakeKit -> {
val yield = getYieldUseCase(integrationId.value).getOrElse {
error("yield must be not null")
}
StakeKitIntegration(integrationId, yield)
}
StakingIntegrationID.P2PEthPool -> {
// TODO p2p avoid network call
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
P2PEthPoolIntegration(integrationId, vaults)
}
}
}
@ -195,7 +205,7 @@ internal class StakingModel @Inject constructor(
return invalidatePendingTransactionsUseCase(
balanceItems = stakeKitBalance?.balance?.items.orEmpty(),
stakingActions = stakingActions,
token = yield.token,
token = integration.token,
).getOrElse { emptyList() }
}
@ -206,7 +216,7 @@ internal class StakingModel @Inject constructor(
stakingBalanceUpdater.create(
cryptoCurrencyStatus,
userWallet,
yield,
integration,
)
}
@ -214,7 +224,7 @@ internal class StakingModel @Inject constructor(
stakingFeeTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
)
}
@ -222,7 +232,7 @@ internal class StakingModel @Inject constructor(
stakingTransactionLoader.create(
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWallet = userWallet,
yield = yield,
integration = integration,
isAmountSubtractAvailable = isAmountSubtractAvailable,
)
}
@ -281,7 +291,7 @@ internal class StakingModel @Inject constructor(
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
when {
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> {
isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch
}
@ -294,12 +304,12 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
).let(::add)
if (yield.args.enter.isPartialAmountDisabled) {
if (integration.isPartialAmountDisabled) {
ValidatorSelectChangeTransformer(
selectedValidator = yield.preferredValidators.firstOrNull(),
yield = yield,
selectedTarget = integration.preferredTargets.firstOrNull(),
integration = integration,
).let(::add)
SetAmountDataTransformer(
clickIntents = this@StakingModel,
@ -314,7 +324,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
).let(::add)
}
}
@ -328,7 +338,7 @@ internal class StakingModel @Inject constructor(
override fun getFee() {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
integration = integration,
appCurrency = appCurrency,
cryptoCurrency = cryptoCurrencyStatus.currency,
),
@ -480,7 +490,7 @@ internal class StakingModel @Inject constructor(
}
override fun onAmountEnterClick() {
if (yield.preferredValidators.isEmpty()) {
if (integration.preferredTargets.isEmpty()) {
stateController.updateEvent(
StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators),
)
@ -488,8 +498,8 @@ internal class StakingModel @Inject constructor(
if (uiState.value.actionType is StakingActionCommonType.Enter) {
stateController.updateAll(
ValidatorSelectChangeTransformer(
selectedValidator = null,
yield = yield,
selectedTarget = null,
integration = integration,
),
)
}
@ -503,7 +513,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
value = value,
yield = yield,
integration = integration,
),
)
}
@ -519,7 +529,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
integration = integration,
),
)
}
@ -540,16 +550,16 @@ internal class StakingModel @Inject constructor(
stakingStateRouter.showValidators()
}
override fun onValidatorSelect(validator: Yield.Validator) {
override fun onTargetSelect(target: StakingTarget) {
analyticsEventHandler.send(
StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name,
validator = target.name,
),
)
stateController.update(
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
)
}
@ -602,7 +612,10 @@ internal class StakingModel @Inject constructor(
override fun onActiveStake(activeStake: BalanceState) {
val networkId = cryptoCurrencyStatus.currency.network.rawId
val preferredValidators = yield.validators.filter { it.preferred }
val preferredValidators = (integration as? StakeKitIntegration)?.targets
?.filterIsInstance<StakingTarget.Validator>()
?.filter { it.delegate.preferred }
.orEmpty()
val pendingActions = activeStake.pendingActions.mapNotNull { action ->
if (action.type in listOf(StakingActionType.RESTAKE, StakingActionType.STAKE) &&
preferredValidators.isSingleItem()
@ -617,7 +630,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingActions = pendingActions,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
onNextClick(activeStake)
@ -630,7 +643,7 @@ internal class StakingModel @Inject constructor(
balanceType = activeStake.type,
pendingAction = action,
balanceState = activeStake,
validator = activeStake.validator,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
)
stateController.update(DismissBottomSheetStateTransformer)
@ -799,7 +812,7 @@ internal class StakingModel @Inject constructor(
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
stakingError = stakingError,
yield = yield,
integration = integration,
),
)
}
@ -922,7 +935,7 @@ internal class StakingModel @Inject constructor(
val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
val feeState = confirmationState?.feeState as? FeeState.Content
val validator = validatorState?.chosenValidator
val target = validatorState?.chosenTarget
val feeAmount = feeState?.fee?.amount
val amount = amountState?.amountTextField?.cryptoAmount
saveBlockchainErrorUseCase(
@ -930,7 +943,7 @@ internal class StakingModel @Inject constructor(
errorMessage = errorMessage,
blockchainId = network.rawId,
derivationPath = network.derivationPath.value,
destinationAddress = validator?.address.orEmpty(),
destinationAddress = target?.address.orEmpty(),
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
@ -939,7 +952,7 @@ internal class StakingModel @Inject constructor(
val email = FeedbackEmailType.StakingProblem(
walletMetaInfo = metaInfo,
validatorName = validator?.name,
validatorName = target?.name,
transactionTypes = transactionsInProgress.map { it.type.name },
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
)
@ -1240,7 +1253,7 @@ internal class StakingModel @Inject constructor(
stateController.updateAll(
SetInitialDataStateTransformer(
clickIntents = this@StakingModel,
yield = yield,
integration = integration,
isAnyTokenStaked = isAnyTokenStaked,
cryptoCurrencyStatus = status,
userWalletProvider = Provider { userWallet },
@ -1259,7 +1272,7 @@ internal class StakingModel @Inject constructor(
balanceState: BalanceState,
pendingActions: ImmutableList<PendingAction> = persistentListOf(),
pendingAction: PendingAction? = pendingActions.firstOrNull(),
validator: Yield.Validator?,
target: StakingTarget?,
amountValue: String,
) {
stateController.updateAll(
@ -1272,11 +1285,11 @@ internal class StakingModel @Inject constructor(
pendingActions = pendingActions,
pendingAction = pendingAction,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
integration = integration,
),
ValidatorSelectChangeTransformer(
selectedValidator = validator,
yield = yield,
selectedTarget = target,
integration = integration,
),
SetAmountDataTransformer(
clickIntents = this,
@ -1291,7 +1304,7 @@ internal class StakingModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = amountValue,
minimumTransactionAmount = minimumTransactionAmount,
yield = yield,
integration = integration,
),
)
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.PendingActionConstraints
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.StakingTarget
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@ -35,10 +35,10 @@ internal data class BalanceState(
val fiatAmount: BigDecimal?,
val formattedFiatAmount: TextReference,
val rawCurrencyId: String?,
val validator: Yield.Validator?,
val target: StakingTarget?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
val validatorAddress: String?,
val targetAddress: String?,
)
@Immutable

View file

@ -10,7 +10,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
@ -97,9 +97,9 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean,
override val isClickable: Boolean,
val isVisibleOnConfirmation: Boolean,
val chosenValidator: Yield.Validator,
val activeValidator: Yield.Validator?,
val availableValidators: List<Yield.Validator>,
val chosenTarget: StakingTarget,
val activeTarget: StakingTarget?,
val availableTargets: List<StakingTarget>,
) : ValidatorState()
data class Empty(

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.BalanceType.Companion.isClickable
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -29,24 +29,24 @@ import java.util.Calendar
internal class BalanceItemConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
value.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = value.getBalanceValue()
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
val title = value.type.getTitle(validator?.name)
val title = value.type.getTitle(target?.name)
return title?.let {
BalanceState(
groupId = value.groupId,
validator = validator,
target = target,
title = title,
subtitle = getSubtitle(value),
type = value.type,
@ -68,7 +68,7 @@ internal class BalanceItemConverter(
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.isClickable(),
isPending = value.isPending,
validatorAddress = value.validatorAddress,
targetAddress = value.validatorAddress,
)
}
}
@ -108,7 +108,7 @@ internal class BalanceItemConverter(
resourceReference(R.string.staking_tap_to_unlock)
}
BalanceType.PREPARING -> {
val warmupPeriod = yield.metadata.warmupPeriod.days
val warmupPeriod = integration.warmupPeriodDays
combinedReference(
resourceReference(R.string.staking_details_warmup_period),
stringReference(" "),
@ -124,7 +124,7 @@ internal class BalanceItemConverter(
}
private fun getUnbondingDate(date: Instant?): TextReference? {
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
val unbondingPeriod = integration.cooldownPeriodDays ?: return null
if (date == null) {
return combinedReference(
resourceReference(R.string.staking_details_unbonding_period),

View file

@ -10,7 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
@ -21,7 +22,7 @@ import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
val stakingBalance = cryptoCurrencyStatus.value.stakingBalance
@ -42,13 +43,13 @@ internal class RewardsValidatorStateConverter(
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
val target = integration.targets.firstOrNull {
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
}
val cryptoValue = balance.amount
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
validator?.toBalanceState(
target?.toBalanceState(
balance = balance,
cryptoCurrencyStatus = cryptoCurrencyStatus,
cryptoValue = cryptoValue,
@ -56,7 +57,7 @@ internal class RewardsValidatorStateConverter(
)
}
private fun Yield.Validator.toBalanceState(
private fun StakingTarget.toBalanceState(
balance: BalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus,
cryptoValue: BigDecimal,
@ -80,7 +81,7 @@ internal class RewardsValidatorStateConverter(
return BalanceState(
groupId = balance.groupId,
validator = this,
target = this,
title = stringReference(this.name),
subtitle = null,
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
@ -93,7 +94,7 @@ internal class RewardsValidatorStateConverter(
isClickable = true,
type = balance.type,
isPending = balance.isPending,
validatorAddress = balance.validatorAddress,
targetAddress = balance.validatorAddress,
)
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.utils.getRewardStakingBalance
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.YieldReward
@ -25,11 +25,11 @@ internal class YieldBalancesConverter(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield,
private val integration: StakingIntegration,
) : Converter<Unit, InnerYieldBalanceState> {
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
override fun convert(value: Unit): InnerYieldBalanceState {

View file

@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
@ -27,7 +27,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
fun updateAfterTransaction() {
coroutineScope.launch {
@ -105,7 +105,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fetchActionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
networkType = yield.token.network,
networkType = integration.token.network,
stakingActionStatus = StakingActionStatus.PROCESSING,
)
}
@ -115,7 +115,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingBalanceUpdater
}

View file

@ -12,12 +12,11 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.EstimateGasUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -45,7 +44,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
) {
suspend fun getFee(
@ -58,9 +57,9 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
?: error("Illegal state")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address
?: state.balanceState?.validatorAddress
?: error("No validator address provided")
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
?: state.balanceState?.targetAddress
?: error("No target address provided")
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
@ -170,11 +169,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = stateController.value.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = sourceAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -234,7 +233,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
): StakingFeeTransactionLoader
}
}

View file

@ -13,15 +13,14 @@ import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
import com.tangem.domain.staking.GetStakingTransactionsUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -56,12 +55,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@Assisted private val integration: StakingIntegration,
@Assisted private val isAmountSubtractAvailable: Boolean,
) {
private val balanceUpdater: StakingBalanceUpdater
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
suspend fun constructAndSendTransactions(
onConstructSuccess: (List<StakingTransaction>) -> Unit,
@ -193,7 +192,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
val amountState = state.amountState as? AmountState.Data
?: error("No amount provided")
val validatorAddress = validatorState.chosenValidator.address
val validatorAddress = validatorState.chosenTarget.address
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
return getStakingTransactionsUseCase(
@ -201,11 +200,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = state.actionType,
integrationId = yield.id,
integrationId = integration.integrationId.value,
amount = amount,
address = defaultAddress,
validatorAddress = validatorAddress,
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
passthrough = action?.passthrough,
type = action?.type,
),
@ -306,7 +305,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
fun create(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
yield: Yield,
integration: StakingIntegration,
isAmountSubtractAvailable: Boolean,
): StakingTransactionSender
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -81,7 +82,7 @@ internal object InitialStakingStatePreview {
fiatAmount = null,
formattedFiatAmount = stringReference("100 $"),
rawCurrencyId = null,
validator = Yield.Validator(
target = Yield.Validator(
address = "address",
status = Yield.Validator.ValidatorStatus.ACTIVE,
name = "Binance",
@ -92,13 +93,13 @@ internal object InitialStakingStatePreview {
votingPower = null,
preferred = false,
isStrategicPartner = false,
),
).toStakingTarget(),
pendingActions = persistentListOf(),
isClickable = true,
type = BalanceType.STAKED,
subtitle = null,
isPending = false,
validatorAddress = "",
targetAddress = "",
),
),
),

View file

@ -1,7 +1,11 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal
@ -13,9 +17,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.054823398040640445"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.1,
stakedBalance = "355544384.45009977",
@ -29,9 +33,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "12495684.05643019",
@ -45,9 +49,9 @@ internal object ValidatorStatePreviewData {
status = ValidatorStatus.ACTIVE,
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
rewardInfo = Yield.RewardInfo(
rewardInfo = RewardInfo(
rate = BigDecimal("0.057786472172836965"),
type = Yield.RewardType.APR,
type = RewardType.APR,
),
commission = 0.05,
stakedBalance = "85400369.96393165",
@ -58,11 +62,13 @@ internal object ValidatorStatePreviewData {
),
)
private val targetList: List<StakingTarget> = validatorList.map { it.toStakingTarget() }
val validatorState = StakingStates.ValidatorState.Data(
availableValidators = validatorList,
chosenValidator = validatorList.first(),
availableTargets = targetList,
chosenTarget = targetList.first(),
isPrimaryButtonEnabled = true,
activeValidator = null,
activeTarget = null,
isClickable = true,
isVisibleOnConfirmation = true,
)

View file

@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
@ -40,7 +40,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {}
override fun onTargetSelect(target: StakingTarget) {}
override fun openRewardsValidators() {}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
@ -24,7 +24,7 @@ internal class SetConfirmationStateInitTransformer(
private val stakingApproval: StakingApproval,
private val stakingAllowance: BigDecimal,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yieldArgs: Yield.Args,
private val integration: StakingIntegration,
private val pendingActions: ImmutableList<PendingAction>? = null,
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
) : Transformer<StakingUiState> {
@ -66,7 +66,7 @@ internal class SetConfirmationStateInitTransformer(
}
private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled
val isPartialEnterAmountDisabled = integration.isPartialAmountDisabled
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
return when {
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
@ -87,12 +87,12 @@ internal class SetConfirmationStateInitTransformer(
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
val isValidatorPreferred = balanceState?.validator?.preferred == true
val isTargetPreferred = balanceState?.target?.isPreferred == true
return if (isSolana && !isValidatorPreferred) {
return if (isSolana && !isTargetPreferred) {
true
} else {
yieldArgs.exit?.isPartialAmountDisabled == true
integration.exitArgs?.isPartialAmountDisabled == true
}
}
}

View file

@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.FeeState
@ -18,7 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedu
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateLoadingTransformer(
private val yield: Yield,
private val integration: StakingIntegration,
private val appCurrency: AppCurrency,
private val cryptoCurrency: CryptoCurrency,
) : Transformer<StakingUiState> {
@ -48,7 +48,7 @@ internal class SetConfirmationStateLoadingTransformer(
)
}
val rewardSchedule = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrency.network.rawId,
decapitalize = true,
)

View file

@ -19,7 +19,10 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.common.RewardClaiming
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -41,7 +44,7 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents,
private val yield: Yield,
private val integration: StakingIntegration,
private val isAnyTokenStaked: Boolean,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val userWalletProvider: Provider<UserWallet>,
@ -55,7 +58,7 @@ internal class SetInitialDataStateTransformer(
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
}
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -63,7 +66,7 @@ internal class SetInitialDataStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyProvider = appCurrencyProvider,
balancesToShowProvider = balancesToShowProvider,
yield = yield,
integration = integration,
)
}
@ -116,27 +119,27 @@ internal class SetInitialDataStateTransformer(
}
private fun createAnnualPercentageItem(): RoundedListWithDividersItemData {
val validators = yield.preferredValidators
val rateRangeInfo = getPercentageRange(validators)
val targets = integration.preferredTargets
val rateRangeInfo = getPercentageRange(targets)
return RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate,
startText = getRateStartText(rateRangeInfo.first),
endText = rateRangeInfo.second,
iconClick = {
when (rateRangeInfo.first) {
Yield.RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
Yield.RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
Yield.RewardType.UNKNOWN -> {}
RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
RewardType.UNKNOWN -> {}
}
},
isEndTextHighlighted = false,
)
}
private fun getRateStartText(rewardType: Yield.RewardType): TextReference {
private fun getRateStartText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}
@ -153,7 +156,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
val cooldownPeriodDays = integration.cooldownPeriodDays ?: return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
@ -169,7 +172,7 @@ internal class SetInitialDataStateTransformer(
private fun createMinimumRequirementItem(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? {
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
val minimumCryptoAmount = integration.enterMinimumAmount ?: return null
val blockchainId = cryptoCurrencyStatus.currency.network.rawId
if (!showMinimumRequirementInfo(blockchainId)) return null
@ -183,7 +186,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
val rewardClaiming = yield.metadata.rewardClaiming
val rewardClaiming = integration.rewardClaiming
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
return RoundedListWithDividersItemData(
@ -195,7 +198,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
val warmupPeriodDays = yield.metadata.warmupPeriod.days
val warmupPeriodDays = integration.warmupPeriodDays
if (warmupPeriodDays == 0) return null
return RoundedListWithDividersItemData(
@ -212,7 +215,7 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
val endTextReference = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
rewardSchedule = integration.rewardSchedule,
networkId = cryptoCurrencyStatus.currency.network.rawId,
decapitalize = false,
) ?: return null
@ -252,15 +255,19 @@ internal class SetInitialDataStateTransformer(
)
}
private fun getPercentageRange(validators: List<Yield.Validator>): Pair<Yield.RewardType, TextReference> {
if (validators.isEmpty()) {
return Yield.RewardType.APR to stringReference(DASH_SIGN)
private fun getPercentageRange(targets: List<StakingTarget>): Pair<RewardType, TextReference> {
if (targets.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val rewardInfos = validators
.filter { it.preferred }
val rewardInfos = targets
.filter { it.isPreferred }
.takeIf { it.isNotEmpty() }
?.mapNotNull { it.rewardInfo }
?: validators.mapNotNull { it.rewardInfo }
?: targets.mapNotNull { it.rewardInfo }
if (rewardInfos.isEmpty()) {
return RewardType.APR to stringReference(DASH_SIGN)
}
val infoWithMinRate = rewardInfos.minBy { it.rate }
val infoWithMaxRate = rewardInfos.maxBy { it.rate }
@ -286,9 +293,9 @@ internal class SetInitialDataStateTransformer(
val EQUALITY_THRESHOLD = BigDecimal(1E-10)
val rewardClaimingResources = mapOf(
Yield.Metadata.RewardClaiming.MANUAL to R.string.staking_reward_claiming_manual,
Yield.Metadata.RewardClaiming.AUTO to R.string.staking_reward_claiming_auto,
Yield.Metadata.RewardSchedule.UNKNOWN to null,
RewardClaiming.MANUAL to R.string.staking_reward_claiming_manual,
RewardClaiming.AUTO to R.string.staking_reward_claiming_auto,
RewardClaiming.UNKNOWN to null,
)
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val value: String,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -41,7 +41,7 @@ internal class AmountChangeStateTransformer(
amountState = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxAmount = maxEnterAmount,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
@ -13,7 +13,7 @@ internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
private val actionType: StakingActionCommonType,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val maxEnterAmountConverter = MaxEnterAmountConverter()
@ -38,7 +38,7 @@ internal class AmountMaxValueStateTransformer(
amountState = AmountRequirementStateTransformer(
maxAmount = maxEnterAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
integration = integration,
actionType = prevState.actionType,
).transform(updatedAmountState),
)

View file

@ -12,8 +12,8 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.common.StakingAmountRequirement
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isTron
@ -26,7 +26,7 @@ import java.math.RoundingMode
internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxAmount: EnterAmountBoundary,
private val yield: Yield,
private val integration: StakingIntegration,
private val actionType: StakingActionCommonType,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -96,11 +96,11 @@ internal class AmountRequirementStateTransformer(
return when (actionType) {
is StakingActionCommonType.Enter -> {
val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
val enterRequirements = integration.enterArgs?.amountRequirement
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
}
is StakingActionCommonType.Exit -> {
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT)
val exitRequirements = integration.exitArgs?.amountRequirement
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
}
else -> null
@ -118,7 +118,7 @@ internal class AmountRequirementStateTransformer(
return isEnterOrExit && isTron && !isIntegerOnly
}
private fun AddressArgument.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? {
private fun StakingAmountRequirement.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? {
val isExceedsMinRequirement = minimum?.compareTo(amount) == 1
val isExceedsMaxRequirement = if (maximum?.isPositive() == true) {
maximum?.compareTo(amount) == -1
@ -142,7 +142,7 @@ internal class AmountRequirementStateTransformer(
return resourceReference(
errorTextRes,
wrappedList(errorText),
).takeIf { required && (isExceedsMinRequirement || isExceedsMaxRequirement) }
).takeIf { isRequired && (isExceedsMinRequirement || isExceedsMaxRequirement) }
}
data class Data(

View file

@ -37,7 +37,7 @@ internal class ShowApprovalBottomSheetTransformer(
val fee = feeState.fee ?: return prevState
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
val validatorAddress = validatorState.chosenValidator.address
val targetAddress = validatorState.chosenTarget.address
val feeCryptoValue = fee.amount.value.format {
crypto(fee.amount.currencySymbol, fee.amount.decimals)
}
@ -57,7 +57,7 @@ internal class ShowApprovalBottomSheetTransformer(
amount = amountState.amountTextField.value,
approveType = ApproveType.UNLIMITED,
walletAddress = walletAddress,
spenderAddress = validatorAddress,
spenderAddress = targetAddress,
fee = resourceReference(
R.string.common_crypto_fiat_format,
wrappedList(feeCryptoValue, feeFiatValue),

View file

@ -15,9 +15,9 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.StakingErrors
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -47,12 +47,12 @@ internal class AddStakingNotificationsTransformer(
private val stakingError: StakingError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
private val yield: Yield,
private val integration: StakingIntegration,
) : Transformer<StakingUiState> {
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
yield = yield,
integration = integration,
isSubtractAvailable = isSubtractAvailable,
)
@ -77,7 +77,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
val minimumRequirement = integration.enterMinimumAmount.orZero()
val sendingAmount = if (isEnterAction) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
@ -26,7 +26,7 @@ import java.math.BigDecimal
internal class StakingInfoNotificationsFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val yield: Yield,
private val integration: StakingIntegration,
private val isSubtractAvailable: Boolean,
) {
@ -94,7 +94,7 @@ internal class StakingInfoNotificationsFactory(
resourceReference(R.string.staking_notification_withdraw_text)
}
StakingActionType.UNLOCK_LOCKED -> {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
if (cooldownPeriodDays != null) {
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
R.string.staking_notification_unlock_text,
@ -213,18 +213,18 @@ internal class StakingInfoNotificationsFactory(
if (prevState.actionType !is StakingActionCommonType.Exit) return
val maxAmount = prevState.balanceState?.cryptoAmount ?: return
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
val exitRequirements = integration.exitArgs?.amountRequirement ?: return
val amountLeft = maxAmount - actionAmount
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
if (exitRequirements.required && isNotEnoughLeft) {
if (exitRequirements.isRequired && isNotEnoughLeft) {
add(StakingNotification.Warning.LowStakedBalance)
}
}
private fun MutableList<NotificationUM>.addUnstakeInfoNotification() {
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
val cooldownPeriodDays = integration.cooldownPeriodDays
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (cooldownPeriodDays != null) {
@ -248,18 +248,17 @@ internal class StakingInfoNotificationsFactory(
val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances
val validatorAddress = prevState.balanceState?.validator?.address ?: return
val targetAddress = prevState.balanceState?.target?.address ?: return
val stakesCountWithCertainValidator = stakingBalances.orEmpty()
.filter {
it.type == BalanceType.STAKED ||
it.type == BalanceType.PREPARING ||
it.type == BalanceType.UNSTAKED
val stakesCountWithCertainTarget = stakingBalances.orEmpty()
.count { state ->
(state.type == BalanceType.STAKED ||
state.type == BalanceType.PREPARING ||
state.type == BalanceType.UNSTAKED) &&
state.target?.address == targetAddress
}
.filter { it.validator?.address == validatorAddress }
.size
if (stakesCountWithCertainValidator > 1) {
if (stakesCountWithCertainTarget > 1) {
add(
StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title),

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.transformers.validator
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -9,8 +10,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class ValidatorSelectChangeTransformer(
private val yield: Yield,
private val selectedValidator: Yield.Validator?,
private val integration: StakingIntegration,
private val selectedTarget: StakingTarget?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -23,27 +24,27 @@ internal class ValidatorSelectChangeTransformer(
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeValidator
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator }
val activeTarget = selectedTarget.takeIf { isFromInfoScreen && isRestake }
?: validatorState?.activeTarget
val filteredTargets = integration.preferredTargets.filterNot { it == activeTarget }
val selectedValidator = if (isRestake && isFromInfoScreen) {
filteredValidators.firstOrNull()
val selectedTarget = if (isRestake && isFromInfoScreen) {
filteredTargets.firstOrNull()
} else {
selectedValidator
selectedTarget
}
if (selectedValidator == null && yield.preferredValidators.isEmpty()) {
if (selectedTarget == null && integration.preferredTargets.isEmpty()) {
return prevState
}
return prevState.copy(
validatorState = StakingStates.ValidatorState.Data(
chosenValidator = selectedValidator ?: yield.preferredValidators.first(),
availableValidators = filteredValidators,
chosenTarget = selectedTarget ?: integration.preferredTargets.first(),
availableTargets = filteredTargets,
isPrimaryButtonEnabled = true,
isClickable = yield.preferredValidators.size > 1,
activeValidator = activeValidator,
isClickable = integration.preferredTargets.size > 1,
activeTarget = activeTarget,
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
),
)

View file

@ -1,11 +1,12 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.extensions.*
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardSchedule
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.SOLANA_SCHEDULE
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.TON_SCHEDULE
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardScheduleConstants.COSMOS_SCHEDULE
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardScheduleConstants.SOLANA_SCHEDULE
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardScheduleConstants.TON_SCHEDULE
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.lib.crypto.BlockchainUtils.isTon
@ -13,57 +14,57 @@ import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
private data object StakingRewardSchedule {
private data object StakingRewardScheduleConstants {
val COSMOS_SCHEDULE = 5 to 12
val SOLANA_SCHEDULE = 2 to 3
val TON_SCHEDULE = 1 to 2
}
internal fun getRewardScheduleText(
rewardSchedule: Yield.Metadata.RewardSchedule,
rewardSchedule: RewardSchedule,
networkId: String,
decapitalize: Boolean,
): TextReference? {
return when (rewardSchedule) {
Yield.Metadata.RewardSchedule.WEEK -> resourceReference(
RewardSchedule.WEEK -> resourceReference(
id = R.string.staking_reward_schedule_week,
decapitalize = decapitalize,
)
Yield.Metadata.RewardSchedule.HOUR -> resourceReference(
RewardSchedule.HOUR -> resourceReference(
id = R.string.staking_reward_schedule_hour,
decapitalize = decapitalize,
)
Yield.Metadata.RewardSchedule.DAY -> resourceReference(
RewardSchedule.DAY -> resourceReference(
id = R.string.staking_reward_schedule_day,
decapitalize = decapitalize,
)
Yield.Metadata.RewardSchedule.MONTH -> resourceReference(
RewardSchedule.MONTH -> resourceReference(
id = R.string.staking_reward_schedule_month,
decapitalize = decapitalize,
)
Yield.Metadata.RewardSchedule.BLOCK,
Yield.Metadata.RewardSchedule.EPOCH,
Yield.Metadata.RewardSchedule.ERA,
RewardSchedule.BLOCK,
RewardSchedule.EPOCH,
RewardSchedule.ERA,
-> getCustomRewardSchedule(
networkId = networkId,
decapitalize = decapitalize,
)
else -> null
RewardSchedule.UNKNOWN -> null
}
}
internal fun getRewardTypeShortText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeShortText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
else -> TextReference.EMPTY
}
}
internal fun getRewardTypeLongText(rewardType: Yield.RewardType): TextReference {
internal fun getRewardTypeLongText(rewardType: RewardType): TextReference {
return when (rewardType) {
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
else -> TextReference.EMPTY
}
}

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -30,7 +30,7 @@ internal fun StakingClaimRewardsValidatorContent(
) {
if (state !is StakingStates.RewardsValidatorsState.Data) return
Column(
modifier = Modifier // Do not put fillMaxSize() in here
modifier = modifier // This function shouldn't itself apply fillMaxSize(); callers should avoid passing it here
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing12)
.verticalScroll(rememberScrollState()),
@ -42,9 +42,9 @@ internal fun StakingClaimRewardsValidatorContent(
caption = item.getAprTextNeutral(),
infoTitle = item.formattedFiatAmount,
infoSubtitle = item.formattedCryptoAmount,
imageUrl = item.validator?.image.orEmpty(),
imageUrl = item.target?.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
modifier = Modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action)
.clickable(
@ -65,11 +65,11 @@ internal fun StakingClaimRewardsValidatorContent(
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -77,6 +77,6 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)

View file

@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -317,11 +317,11 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
@Suppress("UnusedPrivateMember")
@Composable
private fun BalanceState.getAprTextColored() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
text = target?.rewardInfo?.rate?.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
@ -329,8 +329,8 @@ private fun BalanceState.getAprTextColored() = combinedReference(
@Composable
private fun BalanceState.getAprTextNeutral() = combinedReference(
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
)
@Composable
@ -347,7 +347,7 @@ private fun BalanceState.getImage() = when (type) {
BalanceType.UNSTAKED,
BalanceType.LOCKED,
-> null
else -> validator?.image
else -> target?.image
}
private val textGradientColors = listOf(

View file

@ -27,7 +27,8 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -54,24 +55,24 @@ internal fun StakingValidatorListContent(
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
if (state is StakingStates.ValidatorState.Data) {
val validators = state.availableValidators
val targets = state.availableTargets
items(
count = validators.size,
key = { validators[it].address },
contentType = { validators[it]::class.java },
count = targets.size,
key = { targets[it].address },
contentType = { targets[it]::class.java },
) { index ->
val item = validators[index]
val item = targets[index]
InputRowImageSelector(
subtitle = stringReference(item.name),
caption = item.getAprTextNeutral(),
imageUrl = item.image.orEmpty(),
isSelected = item == state.chosenValidator,
onSelect = { clickIntents.onValidatorSelect(item) },
isSelected = item == state.chosenTarget,
onSelect = { clickIntents.onTargetSelect(item) },
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = validators.lastIndex,
lastIndex = targets.lastIndex,
radius = TangemTheme.dimens.radius12,
addDefaultPadding =
false,
@ -106,21 +107,21 @@ internal fun StakingValidatorListContent(
*/
@Suppress("UnusedPrivateMember")
@Composable
private fun Yield.Validator.getAprTextColored() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
private fun StakingTarget.getAprTextColored() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: RewardType.UNKNOWN),
annotatedReference {
appendSpace()
appendColored(
text = rewardInfo?.rate?.orZero().format { percent() },
text = rewardInfo?.rate.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
)
@Composable
private fun Yield.Validator.getAprTextNeutral() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + rewardInfo?.rate?.orZero().format { percent() }),
private fun StakingTarget.getAprTextNeutral() = combinedReference(
getRewardTypeLongText(rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + rewardInfo?.rate.orZero().format { percent() }),
)
@Composable

View file

@ -25,8 +25,9 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates.ValidatorState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.utils.getRewardTypeShortText
import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder
@ -63,20 +64,20 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic
modifier = Modifier.padding(12.dp),
) {
InputRowAsyncImage(
imageUrl = validatorState.chosenValidator.image.orEmpty(),
imageUrl = state.chosenTarget.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = Modifier
.size(24.dp)
.clip(TangemTheme.shapes.roundedCornersXLarge),
)
Text(
text = validatorState.chosenValidator.name,
text = state.chosenTarget.name,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerWMax()
Text(
text = validatorState.getInfoTitleNeutral().resolveReference(),
text = state.getInfoTitleNeutral().resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
@ -91,16 +92,16 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic
@Composable
private fun StakingStates.ValidatorState.Data.getInfoTitleColored() = combinedReference(
annotatedReference {
append(getRewardTypeShortText(chosenValidator.rewardInfo?.type ?: Yield.RewardType.UNKNOWN).resolveReference())
append(getRewardTypeShortText(chosenTarget.rewardInfo?.type ?: RewardType.UNKNOWN).resolveReference())
appendSpace()
appendColored(
text = chosenValidator.rewardInfo?.rate.orZero().format { percent() },
text = chosenTarget.rewardInfo?.rate.orZero().format { percent() },
color = TangemTheme.colors.text.accent,
)
},
)
private fun StakingStates.ValidatorState.Data.getInfoTitleNeutral() = combinedReference(
getRewardTypeShortText(chosenValidator.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
stringReference(" " + chosenValidator.rewardInfo?.rate?.orZero().format { percent() }),
getRewardTypeShortText(chosenTarget.rewardInfo?.type ?: RewardType.UNKNOWN),
stringReference(" " + chosenTarget.rewardInfo?.rate.orZero().format { percent() }),
)

View file

@ -20,7 +20,7 @@ import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.test.SwapSelectNetworkFeeBottomSheetTestTags
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
import com.tangem.feature.swap.models.states.FeeItemState
@ -91,7 +91,7 @@ private fun FooterBlock(readMore: TextReference, onReadMoreClick: () -> Unit) {
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing16,
)
.testTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT),
.testTag(SwapSelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT),
style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start),
onClick = click,
)

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@ -266,11 +267,15 @@ private fun Content(
when (type) {
is TransactionCardType.ReadOnly -> {
if (textFieldValue != null) {
ResizableText(
Text(
text = textFieldValue.text,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
autoSize = TextAutoSize.StepBased(
minFontSize = 16.sp,
maxFontSize = TangemTheme.typography.h2.fontSize,
),
maxLines = 1,
modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD),
)
} else {
@ -608,7 +613,7 @@ private fun TransactionCardPreviewWithPriceImpact() {
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
balance = "123",
textFieldValue = TextFieldValue(),
textFieldValue = TextFieldValue("1000000.0000000000000000000000000"),
priceImpact = PriceImpact.Value(0.15F),
)
}

View file

@ -8,7 +8,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -241,16 +240,10 @@ private fun ProviderItem(index: Int, state: ProviderUM, onDrop: (Int, Int) -> Un
modifier = Modifier
.background(color = color, shape = RoundedCornerShape(16.dp))
.padding(vertical = 4.dp, horizontal = 8.dp)
.dragAndDropSource {
detectTapGestures(
onLongPress = {
startTransfer(
DragAndDropTransferData(
clipData = ClipData.newPlainText("provider index", index.toString()),
flags = View.DRAG_FLAG_GLOBAL,
),
)
},
.dragAndDropSource { _ ->
DragAndDropTransferData(
clipData = ClipData.newPlainText("provider index", index.toString()),
flags = View.DRAG_FLAG_GLOBAL,
)
}
.dragAndDropTarget(

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -510,9 +511,6 @@ private fun LoadingBlock(modifier: Modifier = Modifier) {
@Composable
private fun ActionButtonWithResizableText(config: ActionButtonConfig, modifier: Modifier = Modifier) {
val fontSizeRange = FontSizeRange(min = 10.sp, max = 14.sp)
var fontSizeValue by remember { mutableFloatStateOf(fontSizeRange.max.value) }
ActionBaseButton(
config = config,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius24),
@ -520,11 +518,12 @@ private fun ActionButtonWithResizableText(config: ActionButtonConfig, modifier:
ActionButtonContent(
config = config,
text = { color ->
ResizableText(
Text(
text = config.text.resolveReference(),
fontSizeValue = fontSizeValue.sp,
fontSizeRange = fontSizeRange,
onFontSizeChange = { fontSizeValue = it },
autoSize = TextAutoSize.StepBased(
minFontSize = 10.sp,
maxFontSize = TangemTheme.typography.button.fontSize,
),
color = color,
overflow = TextOverflow.Ellipsis,
maxLines = 1,

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.router
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
@ -37,12 +38,16 @@ internal class DefaultTokenDetailsRouter @Inject constructor(
)
}
override fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yieldId: String) {
override fun openStaking(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
integrationId: StakingIntegrationID,
) {
router.push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = yieldId,
integrationId = integrationId,
),
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.router
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
@ -16,5 +17,5 @@ internal interface InnerTokenDetailsRouter {
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yieldId: String)
fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, integrationId: StakingIntegrationID)
}

View file

@ -54,7 +54,6 @@ import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
@ -124,7 +123,6 @@ internal class TokenDetailsModel @Inject constructor(
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val associateAssetUseCase: AssociateAssetUseCase,
@ -1119,18 +1117,28 @@ internal class TokenDetailsModel @Inject constructor(
private fun openStaking() {
modelScope.launch {
getYieldUseCase.invoke(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
).onRight { yield ->
router.openStaking(userWalletId, cryptoCurrency, yield.id)
}.onLeft {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title)))
}
getStakingAvailabilityUseCase.invokeSync(userWalletId, cryptoCurrency)
.onRight { availability ->
val option = (availability as? StakingAvailability.Available)?.option
if (option != null) {
router.openStaking(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
integrationId = option.integrationId,
)
} else {
showStakingUnavailable()
}
}
.onLeft { showStakingUnavailable() }
}
}
private fun showStakingUnavailable() {
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title)))
}
private fun checkForActionUpdates() {
combine(
tokenDetailsDeepLinkActionListener.tokenDetailsActionFlow,

View file

@ -62,12 +62,12 @@ internal class TokenDetailsStakingInfoConverter(
val hasPendingBalances = when (stakingBalance) {
is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.isNotEmpty()
is StakingBalance.Data.P2P -> !stakingBalance.unstakingAmount.isNullOrZero()
is StakingBalance.Data.P2PEthPool -> !stakingBalance.unstakingAmount.isNullOrZero()
null -> false
}
val pendingAmount = when (stakingBalance) {
is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.sumOf { it.amount }
is StakingBalance.Data.P2P -> stakingBalance.unstakingAmount
is StakingBalance.Data.P2PEthPool -> stakingBalance.unstakingAmount
null -> BigDecimal.ZERO
}
@ -98,7 +98,7 @@ internal class TokenDetailsStakingInfoConverter(
stakingAmount = stakingCryptoAmount,
rewardAmount = when (stakingBalance) {
is StakingBalance.Data.StakeKit -> stakingBalance.getRewardStakingBalance()
is StakingBalance.Data.P2P -> stakingBalance.totalRewards
is StakingBalance.Data.P2PEthPool -> stakingBalance.totalRewards
else -> BigDecimal.ZERO
},
)

View file

@ -505,6 +505,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
) {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
val integrationId = option?.integrationId ?: return
modelScope.launch {
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -512,7 +514,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
yieldId = option?.integrationId ?: return@launch,
integrationId = integrationId,
),
)
}

View file

@ -5,7 +5,6 @@ import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.unit.Density
@ -23,7 +22,7 @@ import kotlin.math.sign
* This position should be considered with regard to the start edge of the item and the placement
* within the viewport.
*
* @return A [SnapLayoutInfoProvider] that can be used with [SnapFlingBehavior]
* @return A [SnapLayoutInfoProvider] that can be used with snap fling behavior
*/
@Suppress("FunctionNaming")
@ExperimentalFoundationApi

View file

@ -2,12 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.*
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton
@ -66,16 +64,10 @@ internal fun LazyListScope.actions(
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
val fontSizeRange = FontSizeRange(min = 10.sp, max = 14.sp)
var fontSizeValue by remember { mutableFloatStateOf(fontSizeRange.max.value) }
actions.fastForEach { action ->
key(action::class.java) {
MultiCurrencyAction(
config = action.config,
fontSizeValue = fontSizeValue.sp,
fontSizeRange = fontSizeRange,
onFontSizeChange = { fontSizeValue = it },
modifier = Modifier.weight(1f),
)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@ -39,9 +40,8 @@ import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.FontSizeRange
import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
@ -69,89 +69,6 @@ private const val HALF_OF_ITEM_WIDTH = 0.5
@Suppress("LongMethod")
@Composable
internal fun WalletCard(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
@Suppress("DestructuringDeclarationWithTooManyEntries")
CardContainer(
dropDownItems = state.dropDownItems,
isLockedState = state is WalletCardState.LockedContent,
modifier = modifier,
) { itemSize ->
val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs()
val contentVerticalMargin = TangemTheme.dimens.spacing12
TitleText(
text = state.title,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(parent.start)
top.linkTo(anchor = parent.top, margin = contentVerticalMargin)
end.linkTo(imageRef.start)
width = Dimension.fillToConstraints
},
)
var balanceWidth by remember { mutableIntStateOf(value = Int.MIN_VALUE) }
Balance(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.onSizeChanged { balanceWidth = it.width }
.padding(vertical = TangemTheme.dimens.spacing8)
.constrainAs(balanceRef) {
start.linkTo(parent.start)
top.linkTo(anchor = titleRef.bottom)
bottom.linkTo(anchor = additionalTextRef.top)
},
)
val additionalText by remember(state.additionalInfo, isBalanceHidden) {
mutableStateOf(
state.additionalInfo?.content?.orMaskWithStars(
maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden,
),
)
}
AdditionalInfo(
text = additionalText,
modifier = Modifier.constrainAs(additionalTextRef) {
start.linkTo(parent.start)
top.linkTo(balanceRef.bottom)
bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin)
if (additionalText != null) {
width = if (state.imageResId != null) {
end.linkTo(imageRef.start)
Dimension.fillToConstraints
} else {
Dimension.wrapContent
}
}
},
)
// If balance has a large width then image must be hidden
val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) {
mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH)
}
if (hasSpaceForImage) {
Image(
id = state.imageResId,
modifier = Modifier.constrainAs(imageRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
},
)
}
}
}
@Composable
private fun CardContainer(
dropDownItems: ImmutableList<WalletDropDownItems>,
isLockedState: Boolean,
modifier: Modifier = Modifier,
content: @Composable (ConstraintLayoutScope.(IntSize) -> Unit),
) {
var isMenuVisible by rememberSaveable { mutableStateOf(value = false) }
var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) }
var itemSize by remember { mutableStateOf(value = IntSize.Zero) }
@ -166,7 +83,7 @@ private fun CardContainer(
.onSizeChanged { itemSize = it }
.testTag(MainScreenTestTags.TOTAL_BALANCE_CONTAINER)
.then(
if (isLockedState || dropDownItems.isEmpty()) {
if (state is WalletCardState.LockedContent || state.dropDownItems.isEmpty()) {
Modifier
} else {
Modifier
@ -189,15 +106,19 @@ private fun CardContainer(
}
},
),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary)
.padding(horizontal = TangemTheme.dimens.spacing12),
) {
content(itemSize)
CardContainer(
state = state,
isBalanceHidden = isBalanceHidden,
itemSize = itemSize,
)
}
}
@ -209,7 +130,82 @@ private fun CardContainer(
pressOffset = pressOffset,
itemHeight = itemHeight,
onDismissRequest = { isMenuVisible = false },
dropDownItems = dropDownItems,
dropDownItems = state.dropDownItems,
)
}
@Suppress("DestructuringDeclarationWithTooManyEntries")
@Composable
private fun ConstraintLayoutScope.CardContainer(state: WalletCardState, isBalanceHidden: Boolean, itemSize: IntSize) {
val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs()
val contentVerticalMargin = TangemTheme.dimens.spacing12
TitleText(
text = state.title,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(anchor = parent.start)
top.linkTo(anchor = parent.top, margin = contentVerticalMargin)
end.linkTo(anchor = imageRef.start)
width = Dimension.fillToConstraints
},
)
var balanceWidth by remember { mutableIntStateOf(value = Int.MIN_VALUE) }
Balance(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.onSizeChanged { balanceWidth = it.width }
.padding(vertical = TangemTheme.dimens.spacing8)
.constrainAs(balanceRef) {
start.linkTo(anchor = parent.start)
top.linkTo(anchor = titleRef.bottom)
bottom.linkTo(anchor = additionalTextRef.top)
},
)
val additionalText by remember(state.additionalInfo, isBalanceHidden) {
mutableStateOf(
state.additionalInfo?.content?.orMaskWithStars(
maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden,
),
)
}
AdditionalInfo(
text = additionalText,
modifier = Modifier.constrainAs(additionalTextRef) {
start.linkTo(parent.start)
top.linkTo(balanceRef.bottom)
bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin)
if (additionalText != null) {
width = if (state.imageResId != null) {
end.linkTo(imageRef.start)
Dimension.fillToConstraints
} else {
Dimension.wrapContent
}
}
},
)
// If balance has a large width then image must be hidden
val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) {
mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH)
}
Image(
id = state.imageResId,
modifier = Modifier.constrainAs(imageRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
visibility = if (hasSpaceForImage) {
Visibility.Visible
} else {
Visibility.Gone
}
},
)
}
@ -282,10 +278,13 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier:
) { balance ->
when (state) {
is WalletCardState.Content -> {
ResizableText(
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
Text(
text = balance,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
autoSize = TextAutoSize.StepBased(
minFontSize = 16.sp,
maxFontSize = TangemTheme.typography.h2.fontSize,
),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.h2

View file

@ -2,13 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.ResizableText
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.buttons.actions.ActionBaseButton
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.buttons.actions.ActionButtonContent
@ -19,13 +19,7 @@ import com.tangem.core.ui.res.TangemTheme
[REDACTED_AUTHOR]
*/
@Composable
internal fun MultiCurrencyAction(
config: ActionButtonConfig,
fontSizeValue: TextUnit,
fontSizeRange: FontSizeRange,
onFontSizeChange: (Float) -> Unit,
modifier: Modifier = Modifier,
) {
internal fun MultiCurrencyAction(config: ActionButtonConfig, modifier: Modifier = Modifier) {
ActionBaseButton(
config = config,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius12),
@ -33,11 +27,12 @@ internal fun MultiCurrencyAction(
ActionButtonContent(
config = config,
text = { color ->
ResizableText(
Text(
text = config.text.resolveReference(),
fontSizeValue = fontSizeValue,
fontSizeRange = fontSizeRange,
onFontSizeChange = onFontSizeChange,
autoSize = TextAutoSize.StepBased(
minFontSize = 10.sp,
maxFontSize = TangemTheme.typography.button.fontSize,
),
color = color,
overflow = TextOverflow.Ellipsis,
maxLines = 1,

View file

@ -14,6 +14,7 @@ import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.impl.R
@ -38,7 +39,7 @@ internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) {
SpacerWMax()
TangemTooltip(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = address,
text = stringReference(address),
enabled = isTooltipEnabled,
content = { contentModifier ->
EllipsisText(

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.impl.R
@ -44,7 +45,7 @@ internal fun WcNetworkItem(networkInfo: WcNetworkInfoUM, modifier: Modifier = Mo
)
TangemTooltip(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = networkInfo.name,
text = stringReference(networkInfo.name),
enabled = isTooltipEnabled,
content = { contentModifier ->
Text(

View file

@ -52,7 +52,7 @@ internal fun WcPortfolioItem(portfolioName: AccountTitleUM, modifier: Modifier =
)
is AccountTitleUM.Text -> TangemTooltip(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = portfolioName.title.resolveReference(),
text = portfolioName.title,
enabled = isTooltipEnabled,
content = { contentModifier ->
Text(