Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-14 15:06:35 +03:00
parent 8009743fb9
commit a72837204d
35 changed files with 628 additions and 132 deletions

View file

@ -5,8 +5,10 @@ import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
@ -109,4 +111,18 @@ object MarketsDomainModule {
fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase {
return GetTokenExchangesUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetStakingNotificationMaxApyUseCase(
settingsRepository: SettingsRepository,
promoRepository: PromoRepository,
marketsTokenRepository: MarketsTokenRepository,
): GetStakingNotificationMaxApyUseCase {
return GetStakingNotificationMaxApyUseCase(
settingsRepository = settingsRepository,
promoRepository = promoRepository,
marketsTokenRepository = marketsTokenRepository,
)
}
}

View file

@ -226,5 +226,13 @@ internal object SettingsDomainModule {
fun provideIsGooglePayAvailableUseCase(settingsRepository: SettingsRepository): IsGooglePayAvailableUseCase {
return IsGooglePayAvailableUseCase(settingsRepository)
}
@Provides
@Singleton
fun provideMaybeSetWalletFirstTimeUsageUseCase(
settingsRepository: SettingsRepository,
): SetWalletFirstTimeUsageUseCase {
return SetWalletFirstTimeUsageUseCase(settingsRepository)
}
// endregion
}

View file

@ -49,12 +49,12 @@ internal class HomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
getUserCountryUseCase: GetUserCountryUseCase,
) : Model() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)

View file

@ -12,6 +12,7 @@ data class TokenMarketListResponse(
@Json(name = "limit") val limit: Int,
@Json(name = "offset") val offset: Int,
@Json(name = "timestamp") val timestamp: Long? = null,
@Json(name = "summary") val summary: Summary? = null,
) {
@JsonClass(generateAdapter = true)
@ -24,6 +25,7 @@ data class TokenMarketListResponse(
@Json(name = "market_rating") val marketRating: Int?,
@Json(name = "market_cap") val marketCap: BigDecimal?,
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
@Json(name = "staking_opportunities") val stakingOpportunities: List<StakingOpportunities>?,
) {
@JsonClass(generateAdapter = true)
@ -32,5 +34,27 @@ data class TokenMarketListResponse(
@Json(name = "1w") val week1: BigDecimal?,
@Json(name = "30d") val day30: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class StakingOpportunities(
@Json(name = "id") val id: Int?,
@Json(name = "apy") val apy: BigDecimal?,
@Json(name = "network_id") val networkId: String?,
@Json(name = "reward_type") val rewardType: RewardType?,
)
@JsonClass(generateAdapter = false)
enum class RewardType {
@Json(name = "apy") APY,
@Json(name = "apr") APR,
UNKNOWN,
}
}
@JsonClass(generateAdapter = true)
data class Summary(
@Json(name = "max_apy") val maxApy: BigDecimal?,
)
}

View file

@ -86,6 +86,12 @@ object PreferencesKeys {
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
booleanPreferencesKey(name = "marketsStakingNotificationHideClicked")
}
val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") }
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") }

View file

@ -321,7 +321,7 @@ private fun SecondaryPairButtons(
}
@Composable
internal fun CloseableIconButton(
fun CloseableIconButton(
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -37,6 +37,7 @@ dependencies {
// endregion
// region Others dependencies
implementation(deps.androidx.datastore)
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.moshi)

View file

@ -27,7 +27,9 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.pagination.*
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
@ -41,6 +43,7 @@ internal class DefaultMarketsTokenRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val cacheRegistry: CacheRegistry,
private val tokenExchangesStore: RuntimeStateStore<List<TokenMarketExchangesResponse.Exchange>>,
private val maxApyStore: RuntimeStateStore<BigDecimal?>,
) : MarketsTokenRepository {
private val tokenMarketInfoConverter: TokenMarketInfoConverter = TokenMarketInfoConverter(excludedBlockchains)
@ -88,8 +91,12 @@ internal class DefaultMarketsTokenRepository(
val last = res.tokens.size < request.limit
val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res)
maxApyStore.store(tokenMarketListWithMaxApy.maxApy)
return BatchFetchResult.Success(
data = TokenMarketListConverter.convert(res),
data = tokenMarketListWithMaxApy.tokens,
last = last,
empty = res.tokens.isEmpty(),
)
@ -275,6 +282,10 @@ internal class DefaultMarketsTokenRepository(
}
}
override suspend fun getMaxApy(): Flow<BigDecimal?> {
return maxApyStore.get()
}
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
return catchErrorAndSendEvent(block, ::createListErrorEvent)
}

View file

@ -15,6 +15,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this)
TokenMarketListConfig.Order.Buyers -> "buyers"
TokenMarketListConfig.Order.TopGainers -> "gainers"
TokenMarketListConfig.Order.TopLosers -> "losers"
TokenMarketListConfig.Order.Staking -> "staking"
}
internal fun PriceChangeInterval.toRequestParam(): String = when (this) {

View file

@ -2,22 +2,29 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListWithMaxApy
import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isPositive
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, TokenMarketListWithMaxApy> {
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
override fun convert(value: TokenMarketListResponse): TokenMarketListWithMaxApy {
val imageHost = value.imageHost ?: run {
if (value.tokens.isEmpty()) {
return emptyList()
return TokenMarketListWithMaxApy(emptyList(), null)
} else {
error("imageHost cannot be null")
}
}
return value.tokens.map { token ->
val tokens = value.tokens.map { token ->
val stakingRate = token.stakingOpportunities
?.mapNotNull { it.apy }
?.max()
.takeIf { it?.isPositive() == true }
TokenMarket(
id = CryptoCurrency.RawID(token.id),
name = token.name,
@ -33,7 +40,9 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, Li
monthChangePercent = token.priceChangePercentage?.day30?.movePointLeft(2),
),
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
stakingRate = stakingRate,
)
}
return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy)
}
}

View file

@ -22,7 +22,7 @@ internal object MarketsDataModule {
@Provides
@Singleton
fun provideMarketsRepository(
fun provideMarketsTokenRepository(
marketsApi: TangemTechMarketsApi,
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
@ -40,6 +40,7 @@ internal object MarketsDataModule {
cacheRegistry = cacheRegistry,
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
excludedBlockchains = excludedBlockchains,
maxApyStore = RuntimeStateStore(defaultValue = null),
)
}
}

View file

@ -67,6 +67,20 @@ internal class DefaultPromoRepository(
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
}
override suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean> {
return appPreferencesStore.get(
key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY,
default = false,
)
}
override suspend fun setMarketsStakingNotificationHideClicked() {
appPreferencesStore.store(
key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY,
value = true,
)
}
override fun getStoryById(id: String): Flow<StoryContent?> = isReadyToShowStories(id).mapLatest {
getStoryByIdSync(id = id, refresh = false)
}

View file

@ -96,6 +96,17 @@ internal class DefaultSettingsRepository(
}
}
override suspend fun getWalletFirstUsageDate(): Long {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY,
default = 0L,
)
}
override suspend fun setWalletFirstUsageDate(value: Long) {
appPreferencesStore.store(key = PreferencesKeys.WALLET_FIRST_USAGE_DATE_KEY, value = value)
}
override suspend fun shouldShowMarketsTooltip(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY,

View file

@ -23,9 +23,11 @@ dependencies {
api(projects.domain.quotes)
api(projects.domain.wallets)
api(projects.domain.wallets.models)
api(projects.domain.promo)
implementation(projects.domain.tokens.models)
implementation(projects.domain.tokens)
implementation(projects.domain.settings)
api(projects.core.pagination)

View file

@ -12,6 +12,7 @@ data class TokenMarket(
val isUnderMarketCapLimit: Boolean,
val tokenQuotesShort: TokenQuotesShort,
val tokenCharts: Charts,
val stakingRate: BigDecimal?,
private val imageHost: String,
) {

View file

@ -8,7 +8,7 @@ data class TokenMarketListConfig(
) {
enum class Order {
ByRating, Trending, Buyers, TopGainers, TopLosers
ByRating, Trending, Buyers, TopGainers, TopLosers, Staking
}
enum class Interval {

View file

@ -0,0 +1,8 @@
package com.tangem.domain.markets
import java.math.BigDecimal
data class TokenMarketListWithMaxApy(
val tokens: List<TokenMarket>,
val maxApy: BigDecimal?,
)

View file

@ -0,0 +1,39 @@
package com.tangem.domain.markets
import android.icu.util.Calendar
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import java.math.BigDecimal
class GetStakingNotificationMaxApyUseCase(
private val settingsRepository: SettingsRepository,
private val promoRepository: PromoRepository,
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(): Flow<BigDecimal?> {
val hideClickedFlow = promoRepository.isMarketsStakingNotificationHideClicked()
val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate()
val currentDate = Calendar.getInstance().timeInMillis
return combine(
flow = hideClickedFlow,
flow2 = marketsTokenRepository.getMaxApy(),
) { hideClicked, maxApy ->
val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) {
currentDate - walletFirstUsageDate < TWO_WEEKS_IN_MILLIS
} else {
false
}
maxApy.takeIf { showStakingNotification }
}
}
private companion object {
const val TWO_WEEKS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.domain.markets.repositories
import com.tangem.domain.markets.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import java.math.BigDecimal
interface MarketsTokenRepository {
@ -51,4 +53,6 @@ interface MarketsTokenRepository {
* @param tokenId token id
*/
suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List<TokenMarketExchange>
suspend fun getMaxApy(): Flow<BigDecimal?>
}

View file

@ -15,6 +15,10 @@ interface PromoRepository {
suspend fun setNeverToShowWalletPromo(promoId: PromoId)
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
suspend fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
suspend fun setMarketsStakingNotificationHideClicked()
// endregion
// region Stories

View file

@ -0,0 +1,16 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
import java.util.Calendar
class SetWalletFirstTimeUsageUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke() = Either.catch {
val savedTime = settingsRepository.getWalletFirstUsageDate()
if (savedTime == 0L) {
settingsRepository.setWalletFirstUsageDate(Calendar.getInstance().timeInMillis)
}
}
}

View file

@ -30,6 +30,10 @@ interface SettingsRepository {
suspend fun incrementAppLaunchCounter()
suspend fun getWalletFirstUsageDate(): Long
suspend fun setWalletFirstUsageDate(value: Long)
suspend fun shouldShowMarketsTooltip(): Boolean
suspend fun setMarketsTooltipShown(value: Boolean)

View file

@ -23,6 +23,7 @@ internal sealed class MarketsListAnalyticsEvent(
SortByTypeUM.ExperiencedBuyers -> "Buyers"
SortByTypeUM.TopGainers -> "Gainers"
SortByTypeUM.TopLosers -> "Losers"
SortByTypeUM.Staking -> "Staking"
},
"Period" to when (interval) {
MarketsListUM.TrendInterval.H24 -> "24h"
@ -31,4 +32,10 @@ internal sealed class MarketsListAnalyticsEvent(
},
),
)
data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo")
data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed")
data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info")
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.markets.tokenlist.impl.model
import androidx.compose.runtime.Stable
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -8,7 +9,13 @@ import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
@ -21,8 +28,10 @@ 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 java.math.BigDecimal
import javax.inject.Inject
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
@ -31,23 +40,26 @@ private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@ModelScoped
@Stable
@Suppress("LongParameterList")
internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase,
private val promoRepository: PromoRepository,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private var updateQuotesJob = JobHolder()
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
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())
@ -57,7 +69,10 @@ internal class MarketsListModel @Inject constructor(
visibleItemsChanged = { visibleItemIds.value = it },
onRetryButtonClicked = { activeListManager.reload() },
onTokenClick = { onTokenUIClicked(it) },
onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) },
onStakingNotificationCloseClick = { onStakingNotificationCloseClick() },
)
private val mainMarketsListManager = MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
@ -92,31 +107,60 @@ internal class MarketsListModel @Inject constructor(
val state = marketsListUMStateManager.state.asStateFlow()
init {
@Suppress("UnnecessaryParentheses")
modelScope.launch {
marketsListUMStateManager.isInSearchStateFlow
.flatMapLatest { isInSearchMode ->
if (isInSearchMode) {
combine(
searchMarketsListManager.uiItems,
searchMarketsListManager.isInInitialLoadingErrorState,
searchMarketsListManager.isSearchNotFoundState,
) { items, isError, notFound ->
(items to isError) to notFound
}
} else {
combine(
mainMarketsListManager.uiItems,
mainMarketsListManager.isInInitialLoadingErrorState,
) { items, isError -> (items to isError) to false }
@Suppress("UnnecessaryParentheses") modelScope.launch {
marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode ->
if (isInSearchMode) {
combine(
searchMarketsListManager.uiItems,
searchMarketsListManager.isInInitialLoadingErrorState,
searchMarketsListManager.isSearchNotFoundState,
getStakingNotificationMaxApyUseCase(),
getUserCountryUseCase.invoke(),
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry ->
MarketsItemsData(
items = uiItems,
isInErrorState = isInInitialLoadingErrorState,
isSearchNotFound = isSearchNotFoundState,
stakingNotificationMaxApy = stakingMaxApy,
userCountry = userCountry,
)
}
} else {
combine(
mainMarketsListManager.uiItems,
mainMarketsListManager.isInInitialLoadingErrorState,
getStakingNotificationMaxApyUseCase(),
getUserCountryUseCase.invoke(),
) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry ->
MarketsItemsData(
items = uiItems,
isInErrorState = isInInitialLoadingErrorState,
isSearchNotFound = false,
stakingNotificationMaxApy = stakingNotificationMaxApy,
userCountry = userCountry,
)
}
}.collect {
marketsListUMStateManager.onUiItemsChanged(
uiItems = it.first.first,
isInErrorState = it.first.second,
isSearchNotFound = it.second,
)
}
}.collect { marketsItemsData ->
val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless {
marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions()
}
if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null &&
stakingNotificationMaxApy != null
) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown)
}
marketsListUMStateManager.onUiItemsChanged(
uiItems = marketsItemsData.items,
isInErrorState = marketsItemsData.isInErrorState,
isSearchNotFound = marketsItemsData.isSearchNotFound,
stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless {
marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions()
},
)
}
}
state.onEach {
@ -126,29 +170,22 @@ internal class MarketsListModel @Inject constructor(
}.launchIn(modelScope)
// update all lists when user's currency has changed
currentAppCurrency
.drop(1)
.onEach {
mainMarketsListManager.reload()
if (marketsListUMStateManager.isInSearchState) {
searchMarketsListManager.reload()
}
}.launchIn(modelScope)
currentAppCurrency.drop(1).onEach {
mainMarketsListManager.reload()
if (marketsListUMStateManager.isInSearchState) {
searchMarketsListManager.reload()
}
}.launchIn(modelScope)
// load charts when new batch is being loaded
mainMarketsListManager.onLastBatchLoadedSuccess
.onEach {
mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
}
.launchIn(modelScope)
mainMarketsListManager.onLastBatchLoadedSuccess.onEach {
mainMarketsListManager.loadCharts(setOf(it), 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)
marketsListUMStateManager.state.map { it.selectedInterval }.distinctUntilChanged().drop(1)
.collectLatest { interval ->
when (marketsListUMStateManager.selectedSortByType) {
SortByTypeUM.Rating -> {
@ -163,66 +200,52 @@ internal class MarketsListModel @Inject constructor(
// reload list when sorting type has changed
modelScope.launch {
marketsListUMStateManager.state
.map { it.selectedSortBy }
.distinctUntilChanged()
.drop(1)
.collectLatest {
mainMarketsListManager.reload()
}
marketsListUMStateManager.state.map { it.selectedSortBy }.distinctUntilChanged().drop(1).collectLatest {
mainMarketsListManager.reload()
}
}
// listen current visible batch and update charts
modelScope.launch {
visibleItemIds
.mapNotNull {
if (it.isNotEmpty()) {
activeListManager.getBatchKeysByItemIds(visibleItemIds.value)
} else {
null
}
}
.distinctUntilChanged()
.collectLatest { visibleBatchKeys ->
// TODO load batch on scroll heat area
activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval)
visibleItemIds.mapNotNull {
if (it.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
}
marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode ->
activeListManager = if (isInSearchMode) {
searchMarketsListManager
} else {
searchMarketsListManager.clearStateAndStopAllActions()
mainMarketsListManager
}
}
}
modelScope.launch {
marketsListUMStateManager.searchQueryFlow
.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
.distinctUntilChanged()
.onEach {
marketsListUMStateManager.searchQueryFlow.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
.distinctUntilChanged().onEach {
if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions()
}
.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }
.collectLatest {
}.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }.collectLatest {
searchMarketsListManager.reload(searchText = it)
}
}
modelScope.launch {
searchMarketsListManager
.onLastBatchLoadedSuccess
.collectLatest {
searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
}
searchMarketsListManager.onLastBatchLoadedSuccess.collectLatest {
searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
}
}
// analytics
@ -233,17 +256,14 @@ internal class MarketsListModel @Inject constructor(
}
private fun initAnalytics() {
containerBottomSheetState
.onEach {
if (it == BottomSheetState.EXPANDED) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened)
}
}.launchIn(modelScope)
containerBottomSheetState.onEach {
if (it == BottomSheetState.EXPANDED) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened)
}
}.launchIn(modelScope)
state
.filter { it.isInSearchMode.not() }
.map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }
.distinctUntilChanged()
state.filter { it.isInSearchMode.not() }
.map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged()
.onEach {
analyticsEventHandler.send(it)
}.launchIn(modelScope)
@ -270,4 +290,19 @@ internal class MarketsListModel @Inject constructor(
}
}.saveIn(updateQuotesJob)
}
private fun onStakingNotificationCloseClick() {
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed)
modelScope.launch {
promoRepository.setMarketsStakingNotificationHideClicked()
}
}
private class MarketsItemsData(
val items: ImmutableList<MarketsListItemUM>,
val isInErrorState: Boolean,
val isSearchNotFound: Boolean,
val stakingNotificationMaxApy: BigDecimal?,
val userCountry: Either<UserCountryError, UserCountry>,
)
}

View file

@ -5,6 +5,8 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -12,6 +14,7 @@ import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
import com.tangem.utils.converter.Converter
@ -37,8 +40,11 @@ internal class MarketsTokenItemConverter(
price = value.getCurrentPrice(),
trendPercentText = value.getTrendPercent(),
trendType = value.getTrendType(),
chardData = value.getChartData(),
chartData = value.getChartData(),
isUnder100kMarketCap = value.isUnderMarketCapLimit,
stakingRate = value.stakingRate?.format { percent() }?.let {
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
},
)
}
@ -64,7 +70,7 @@ internal class MarketsTokenItemConverter(
prevUI.trendPercentText,
) { new.getTrendPercent() },
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() },
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
)
}

View file

@ -323,6 +323,7 @@ internal class MarketsListBatchFlowManager(
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
}
}

View file

@ -16,14 +16,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import java.math.BigDecimal
@Stable
@Suppress("LongParameterList")
internal class MarketsListUMStateManager(
private val currentVisibleIds: Provider<List<CryptoCurrency.RawID>>,
private val onLoadMoreUiItems: () -> Unit,
private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> Unit,
private val onRetryButtonClicked: () -> Unit,
private val onTokenClick: (MarketsListItemUM) -> Unit,
private val onStakingNotificationClick: () -> Unit,
private val onStakingNotificationCloseClick: () -> Unit,
) {
private var sortByBottomSheetIsShown
@ -87,6 +91,7 @@ internal class MarketsListUMStateManager(
isInErrorState: Boolean,
isSearchNotFound: Boolean,
uiItems: ImmutableList<MarketsListItemUM>,
stakingNotificationMaxApy: BigDecimal?,
) {
state.update {
when {
@ -102,13 +107,19 @@ internal class MarketsListUMStateManager(
it.copy(list = ListUM.Loading)
}
else -> {
it.updateItems(newItems = uiItems)
it.updateItems(
newItems = uiItems,
stakingNotificationMaxApy = stakingNotificationMaxApy,
)
}
}
}
}
private fun MarketsListUM.updateItems(newItems: ImmutableList<MarketsListItemUM>): MarketsListUM {
private fun MarketsListUM.updateItems(
newItems: ImmutableList<MarketsListItemUM>,
stakingNotificationMaxApy: BigDecimal?,
): MarketsListUM {
val currentState = this
if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) {
@ -119,6 +130,7 @@ internal class MarketsListUMStateManager(
.copy(
showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(),
),
stakingNotificationMaxApy = stakingNotificationMaxApy,
)
}
@ -211,6 +223,12 @@ internal class MarketsListUMStateManager(
onOptionClicked = ::onBottomSheetOptionClicked,
),
),
stakingNotificationMaxApy = null,
onStakingNotificationClick = {
onStakingNotificationClick()
selectedSortByType = SortByTypeUM.Staking
},
onStakingNotificationCloseClick = onStakingNotificationCloseClick,
)
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.buildAnnotatedString
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
@ -29,10 +30,11 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
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.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -41,6 +43,7 @@ import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
@ -48,6 +51,9 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
private const val SHOW_MORE_KEY = "privacyPolicy"
@Composable
internal fun MarketsList(
@ -110,14 +116,50 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
SpacerH12()
}
}
AnimatedVisibility(state.isInSearchMode.not()) {
Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
sortByTypeUM = state.selectedSortBy,
trendInterval = state.selectedInterval,
onIntervalClick = state.onIntervalClick,
onSortByClick = state.onSortByButtonClick,
)
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,
)
}
AnimatedVisibility(
state.isInSearchMode.not() &&
state.stakingNotificationMaxApy != null &&
state.selectedSortBy != SortByTypeUM.Staking,
) {
val showMore = stringResourceSafe(R.string.markets_staking_banner_description_show_more)
val description = stringResourceSafe(
R.string.markets_staking_banner_description_placeholder,
showMore,
)
val clickableDescription = buildAnnotatedString {
append(description.substringBefore(showMore))
pushStringAnnotation(SHOW_MORE_KEY, "")
appendColored(showMore, TangemTheme.colors.text.accent)
pop()
}
StakingInMarketsPromoNotification(
config = NotificationConfig(
iconResId = R.drawable.img_staking_in_market_notification,
title = resourceReference(
R.string.markets_staking_banner_title,
wrappedList(state.stakingNotificationMaxApy.format { percent() }),
),
subtitle = annotatedReference(clickableDescription),
onClick = state.onStakingNotificationClick,
onCloseClick = state.onStakingNotificationCloseClick,
),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
}
}
val strokeWidth = TangemTheme.dimens.size0_5
@ -326,6 +368,9 @@ private fun Preview() {
onDismissRequest = {},
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
),
stakingNotificationMaxApy = BigDecimal(0.12345),
onStakingNotificationClick = {},
onStakingNotificationCloseClick = {},
),
onHeaderSizeChange = {},
bottomSheetState = BottomSheetState.EXPANDED,

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
@ -22,6 +23,8 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -95,6 +98,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier
.alignByBaseline(),
ratingPosition = model.ratingPosition,
marketCap = model.marketCap,
stakingRate = model.stakingRate,
)
PriceChangeInPercent(
modifier = Modifier.alignByBaseline(),
@ -110,7 +114,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier
Chart(
chartType = model.chartType,
chartRawData = model.chardData,
chartRawData = model.chartData,
)
}
}
@ -142,7 +146,12 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier
}
@Composable
private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) {
private fun TokenSubtitle(
ratingPosition: String?,
marketCap: String?,
stakingRate: TextReference?,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
@ -150,6 +159,10 @@ private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier:
TokenRatingPlace(ratingPosition = ratingPosition)
SpacerW4()
TokenMarketCapText(text = marketCap ?: "")
if (stakingRate != null) {
SpacerW4()
StakingRate(stakingRate = stakingRate.resolveReference())
}
}
}
@ -174,6 +187,28 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
}
}
@Composable
private fun RowScope.StakingRate(stakingRate: String) {
Box(
modifier = Modifier
.alignByBaseline()
.heightIn(min = TangemTheme.dimens.size16)
.border(
width = TangemTheme.dimens.size1,
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersSmall2,
)
.padding(horizontal = TangemTheme.dimens.spacing5),
) {
Text(
text = stakingRate,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption1,
maxLines = 1,
)
}
}
@Composable
private fun RowScope.TokenMarketCapText(text: String) {
Text(

View file

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

View file

@ -4,6 +4,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import kotlinx.collections.immutable.persistentListOf
@ -20,10 +21,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
chartData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
MarketsListItemUM(
id = CryptoCurrency.RawID("1"),
@ -35,8 +37,9 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.NEUTRAL,
chardData = null,
chartData = null,
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
MarketsListItemUM(
id = CryptoCurrency.RawID("1"),
@ -48,10 +51,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.DOWN,
chardData = MarketChartRawData(
chartData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
MarketsListItemUM(
id = CryptoCurrency.RawID("1"),
@ -63,10 +67,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
chartData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
MarketsListItemUM(
id = CryptoCurrency.RawID("1"),
@ -78,10 +83,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
chartData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
MarketsListItemUM(
id = CryptoCurrency.RawID("1"),
@ -93,10 +99,11 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
price = MarketsListItemUM.Price(text = "31 285.72$"),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chardData = MarketChartRawData(
chartData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
stakingRate = stringReference("APY 12.34%"),
),
),
)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
@Immutable
@ -17,8 +18,9 @@ data class MarketsListItemUM(
val price: Price,
val trendPercentText: String,
val trendType: PriceChangeType,
val chardData: MarketChartRawData?,
val chartData: MarketChartRawData?,
val isUnder100kMarketCap: Boolean,
val stakingRate: TextReference?,
) {
val chartType: MarketChartLook.Type = when (trendType) {
PriceChangeType.UP -> MarketChartLook.Type.Growing

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
internal data class MarketsListUM(
val list: ListUM,
@ -18,6 +19,9 @@ internal data class MarketsListUM(
val selectedInterval: TrendInterval,
val onIntervalClick: (TrendInterval) -> Unit,
val onSortByButtonClick: () -> Unit,
val stakingNotificationMaxApy: BigDecimal?,
val onStakingNotificationClick: () -> Unit,
val onStakingNotificationCloseClick: () -> Unit,
) {
val isInSearchMode
get() = searchBar.isActive
@ -35,6 +39,7 @@ enum class SortByTypeUM(val text: TextReference) {
ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)),
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
Staking(resourceReference(R.string.common_staking)),
}
@Immutable

View file

@ -67,6 +67,7 @@ internal class WalletModel @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase,
private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase,
private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase,
private val setWalletFirstTimeUsageUseCase: SetWalletFirstTimeUsageUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -103,6 +104,7 @@ internal class WalletModel @Inject constructor(
suggestToOpenMarkets()
maybeMigrateNames()
maybeSetWalletFirstTimeUsage()
subscribeToUserWalletsUpdates()
subscribeOnBalanceHiding()
subscribeOnSelectedWalletFlow()
@ -117,6 +119,12 @@ internal class WalletModel @Inject constructor(
}
}
private fun maybeSetWalletFirstTimeUsage() {
modelScope.launch {
setWalletFirstTimeUsageUseCase()
}
}
override fun onDestroy() {
super.onDestroy()