Updated on 2026-08-14
This commit is contained in:
commit
e388c14eb9
184 changed files with 4338 additions and 1298 deletions
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }),
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue