Updated on 2026-08-14
This commit is contained in:
parent
ed89b4bcd0
commit
533c036819
28 changed files with 334 additions and 162 deletions
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
|
||||||
import com.tangem.domain.staking.StakingIdFactory
|
import com.tangem.domain.staking.StakingIdFactory
|
||||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
|
|
@ -129,13 +128,11 @@ object MarketsDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGetStakingNotificationMaxApyUseCase(
|
fun provideShouldShowYieldModeMarketPromoUseCase(
|
||||||
settingsRepository: SettingsRepository,
|
|
||||||
promoRepository: PromoRepository,
|
promoRepository: PromoRepository,
|
||||||
marketsTokenRepository: MarketsTokenRepository,
|
marketsTokenRepository: MarketsTokenRepository,
|
||||||
): GetStakingNotificationMaxApyUseCase {
|
): ShouldShowYieldModeMarketPromoUseCase {
|
||||||
return GetStakingNotificationMaxApyUseCase(
|
return ShouldShowYieldModeMarketPromoUseCase(
|
||||||
settingsRepository = settingsRepository,
|
|
||||||
promoRepository = promoRepository,
|
promoRepository = promoRepository,
|
||||||
marketsTokenRepository = marketsTokenRepository,
|
marketsTokenRepository = marketsTokenRepository,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ data class TokenMarketListResponse(
|
||||||
@Json(name = "market_cap") val marketCap: BigDecimal?,
|
@Json(name = "market_cap") val marketCap: BigDecimal?,
|
||||||
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
|
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
|
||||||
@Json(name = "staking_opportunities") val stakingOpportunities: List<StakingOpportunities>?,
|
@Json(name = "staking_opportunities") val stakingOpportunities: List<StakingOpportunities>?,
|
||||||
|
@Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ object PreferencesKeys {
|
||||||
|
|
||||||
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
|
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
|
||||||
|
|
||||||
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
|
val MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
|
||||||
booleanPreferencesKey(name = "marketsStakingNotificationHideClicked")
|
booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") }
|
val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") }
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -20,6 +20,7 @@ import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||||
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
|
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
|
||||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.*
|
import com.tangem.domain.markets.*
|
||||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||||
import com.tangem.domain.models.account.DerivationIndex
|
import com.tangem.domain.models.account.DerivationIndex
|
||||||
|
|
@ -28,9 +29,8 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.pagination.*
|
import com.tangem.pagination.*
|
||||||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.math.BigDecimal
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
|
|
@ -43,7 +43,6 @@ internal class DefaultMarketsTokenRepository(
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val cacheRegistry: CacheRegistry,
|
private val cacheRegistry: CacheRegistry,
|
||||||
private val tokenExchangesStore: RuntimeStateStore<List<TokenMarketExchangesResponse.Exchange>>,
|
private val tokenExchangesStore: RuntimeStateStore<List<TokenMarketExchangesResponse.Exchange>>,
|
||||||
private val maxApyStore: RuntimeStateStore<BigDecimal?>,
|
|
||||||
private val networkFactory: NetworkFactory,
|
private val networkFactory: NetworkFactory,
|
||||||
excludedBlockchains: ExcludedBlockchains,
|
excludedBlockchains: ExcludedBlockchains,
|
||||||
) : MarketsTokenRepository {
|
) : MarketsTokenRepository {
|
||||||
|
|
@ -95,8 +94,6 @@ internal class DefaultMarketsTokenRepository(
|
||||||
|
|
||||||
val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res)
|
val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res)
|
||||||
|
|
||||||
maxApyStore.store(tokenMarketListWithMaxApy.maxApy)
|
|
||||||
|
|
||||||
return BatchFetchResult.Success(
|
return BatchFetchResult.Success(
|
||||||
data = tokenMarketListWithMaxApy.tokens,
|
data = tokenMarketListWithMaxApy.tokens,
|
||||||
last = last,
|
last = last,
|
||||||
|
|
@ -294,8 +291,24 @@ internal class DefaultMarketsTokenRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMaxApy(): Flow<BigDecimal?> {
|
override suspend fun showYieldModePromo(
|
||||||
return maxApyStore.get()
|
appCurrency: AppCurrency,
|
||||||
|
interval: TokenMarketListConfig.Interval,
|
||||||
|
): Boolean = try {
|
||||||
|
val hasYieldSupplyTokens = marketsApi.getCoinsList(
|
||||||
|
currency = appCurrency.code,
|
||||||
|
interval = interval.toRequestParam(),
|
||||||
|
order = TokenMarketListConfig.Order.YieldSupply.toRequestParam(),
|
||||||
|
offset = 0,
|
||||||
|
limit = 40,
|
||||||
|
timestamp = null,
|
||||||
|
search = null,
|
||||||
|
).getOrThrow().tokens.isNotEmpty()
|
||||||
|
|
||||||
|
hasYieldSupplyTokens
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Timber.e(error)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
|
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this)
|
||||||
TokenMarketListConfig.Order.TopGainers -> "gainers"
|
TokenMarketListConfig.Order.TopGainers -> "gainers"
|
||||||
TokenMarketListConfig.Order.TopLosers -> "losers"
|
TokenMarketListConfig.Order.TopLosers -> "losers"
|
||||||
TokenMarketListConfig.Order.Staking -> "staking"
|
TokenMarketListConfig.Order.Staking -> "staking"
|
||||||
|
TokenMarketListConfig.Order.YieldSupply -> "yield"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PriceChangeInterval.toRequestParam(): String = when (this) {
|
internal fun PriceChangeInterval.toRequestParam(): String = when (this) {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, To
|
||||||
monthChangePercent = token.priceChangePercentage?.day30?.movePointLeft(2),
|
monthChangePercent = token.priceChangePercentage?.day30?.movePointLeft(2),
|
||||||
),
|
),
|
||||||
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
|
tokenCharts = TokenMarket.Charts(h24 = null, week = null, month = null),
|
||||||
stakingRate = stakingRate,
|
yieldRate = stakingRate ?: token.maxYieldApy?.movePointLeft(2),
|
||||||
updateTimestamp = value.timestamp,
|
updateTimestamp = value.timestamp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ internal object MarketsDataModule {
|
||||||
cacheRegistry = cacheRegistry,
|
cacheRegistry = cacheRegistry,
|
||||||
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
|
tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()),
|
||||||
excludedBlockchains = excludedBlockchains,
|
excludedBlockchains = excludedBlockchains,
|
||||||
maxApyStore = RuntimeStateStore(defaultValue = null),
|
|
||||||
networkFactory = networkFactory,
|
networkFactory = networkFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,16 +90,16 @@ internal class DefaultPromoRepository(
|
||||||
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
|
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isMarketsStakingNotificationHideClicked(): Flow<Boolean> {
|
override fun isMarketsYieldSupplyNotificationHideClicked(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(
|
return appPreferencesStore.get(
|
||||||
key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY,
|
key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY,
|
||||||
default = false,
|
default = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setMarketsStakingNotificationHideClicked() {
|
override suspend fun setMarketsYieldSupplyNotificationHideClicked() {
|
||||||
appPreferencesStore.store(
|
appPreferencesStore.store(
|
||||||
key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY,
|
key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY,
|
||||||
value = true,
|
value = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ data class TokenMarket(
|
||||||
val isUnderMarketCapLimit: Boolean,
|
val isUnderMarketCapLimit: Boolean,
|
||||||
val tokenQuotesShort: TokenQuotesShort,
|
val tokenQuotesShort: TokenQuotesShort,
|
||||||
val tokenCharts: Charts,
|
val tokenCharts: Charts,
|
||||||
val stakingRate: BigDecimal?,
|
val yieldRate: BigDecimal?,
|
||||||
val updateTimestamp: Long?,
|
val updateTimestamp: Long?,
|
||||||
private val imageHost: String,
|
private val imageHost: String,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ data class TokenMarketListConfig(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
enum class Order {
|
enum class Order {
|
||||||
ByRating, Trending, Buyers, TopGainers, TopLosers, Staking
|
ByRating, Trending, Buyers, TopGainers, TopLosers, Staking, YieldSupply,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Interval {
|
enum class Interval {
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.tangem.domain.markets
|
||||||
|
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||||
|
import com.tangem.domain.promo.PromoRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class ShouldShowYieldModeMarketPromoUseCase(
|
||||||
|
private val promoRepository: PromoRepository,
|
||||||
|
private val marketsTokenRepository: MarketsTokenRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Flow<Boolean> {
|
||||||
|
val hideClickedFlow = promoRepository.isMarketsYieldSupplyNotificationHideClicked()
|
||||||
|
|
||||||
|
return hideClickedFlow.map { hideClicked ->
|
||||||
|
!hideClicked && marketsTokenRepository.showYieldModePromo(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
interval = interval,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package com.tangem.domain.markets.repositories
|
package com.tangem.domain.markets.repositories
|
||||||
|
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.*
|
import com.tangem.domain.markets.*
|
||||||
import com.tangem.domain.models.account.DerivationIndex
|
import com.tangem.domain.models.account.DerivationIndex
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
interface MarketsTokenRepository {
|
interface MarketsTokenRepository {
|
||||||
|
|
||||||
|
|
@ -56,5 +55,5 @@ interface MarketsTokenRepository {
|
||||||
*/
|
*/
|
||||||
suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List<TokenMarketExchange>
|
suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List<TokenMarketExchange>
|
||||||
|
|
||||||
suspend fun getMaxApy(): Flow<BigDecimal?>
|
suspend fun showYieldModePromo(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -16,9 +16,9 @@ interface PromoRepository {
|
||||||
|
|
||||||
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
|
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
|
||||||
|
|
||||||
fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
|
fun isMarketsYieldSupplyNotificationHideClicked(): Flow<Boolean>
|
||||||
|
|
||||||
suspend fun setMarketsStakingNotificationHideClicked()
|
suspend fun setMarketsYieldSupplyNotificationHideClicked()
|
||||||
|
|
||||||
suspend fun isMoonpayPromoActive(): Boolean
|
suspend fun isMoonpayPromoActive(): Boolean
|
||||||
// endregion
|
// endregion
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter(
|
||||||
trendType = value.getTrendType(),
|
trendType = value.getTrendType(),
|
||||||
chartData = value.getChartData(),
|
chartData = value.getChartData(),
|
||||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||||
stakingRate = value.stakingRate?.format { percent() }?.let {
|
stakingRate = value.yieldRate?.format { percent() }?.let {
|
||||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||||
},
|
},
|
||||||
updateTimestamp = value.updateTimestamp,
|
updateTimestamp = value.updateTimestamp,
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,7 @@ internal class FeedComponentModel @Inject constructor(
|
||||||
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
||||||
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
||||||
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
||||||
|
SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@ internal class FeedMarketsBatchFlowManager(
|
||||||
TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers
|
TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers
|
||||||
TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers
|
TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers
|
||||||
TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking
|
TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking
|
||||||
|
TokenMarketListConfig.Order.YieldSupply -> SortByTypeUM.YieldSupply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ enum class SortByTypeUM(val text: TextReference) {
|
||||||
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
||||||
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
||||||
Staking(resourceReference(R.string.common_staking)),
|
Staking(resourceReference(R.string.common_staking)),
|
||||||
|
YieldSupply(resourceReference(R.string.markets_sort_by_yield_mode_title)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ internal sealed class MarketsListAnalyticsEvent(
|
||||||
SortByTypeUM.TopGainers -> "Gainers"
|
SortByTypeUM.TopGainers -> "Gainers"
|
||||||
SortByTypeUM.TopLosers -> "Losers"
|
SortByTypeUM.TopLosers -> "Losers"
|
||||||
SortByTypeUM.Staking -> "Staking"
|
SortByTypeUM.Staking -> "Staking"
|
||||||
|
SortByTypeUM.YieldSupply -> "Yield Supply"
|
||||||
},
|
},
|
||||||
"Period" to when (interval) {
|
"Period" to when (interval) {
|
||||||
MarketsListUM.TrendInterval.H24 -> "24h"
|
MarketsListUM.TrendInterval.H24 -> "24h"
|
||||||
|
|
@ -32,12 +33,11 @@ internal sealed class MarketsListAnalyticsEvent(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
class YieldModePromoShown : MarketsListAnalyticsEvent(event = "Notice - Yield Mode Promo")
|
||||||
|
|
||||||
class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo")
|
class YieldModePromoClosed : MarketsListAnalyticsEvent(event = "Yield Mode Promo Closed")
|
||||||
|
|
||||||
class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed")
|
class YieldModeMoreInfoClicked : MarketsListAnalyticsEvent(event = "Yield Mode More Info")
|
||||||
|
|
||||||
class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info")
|
|
||||||
|
|
||||||
data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent(
|
data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent(
|
||||||
event = "Token Searched",
|
event = "Token Searched",
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,20 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||||
import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase
|
import com.tangem.domain.markets.ShouldShowYieldModeMarketPromoUseCase
|
||||||
import com.tangem.domain.markets.TokenMarket
|
import com.tangem.domain.markets.TokenMarket
|
||||||
|
import com.tangem.domain.markets.TokenMarketListConfig
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||||
import com.tangem.domain.settings.usercountry.models.UserCountryError
|
import com.tangem.domain.settings.usercountry.models.UserCountryError
|
||||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
|
||||||
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
|
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
|
||||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
|
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
|
||||||
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
|
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
|
||||||
|
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
|
@ -31,7 +32,6 @@ import com.tangem.utils.coroutines.saveIn
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import java.math.BigDecimal
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
|
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
|
||||||
|
|
@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor(
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase,
|
shouldShowYieldModeMarketPromoUseCase: ShouldShowYieldModeMarketPromoUseCase,
|
||||||
private val promoRepository: PromoRepository,
|
private val promoRepository: PromoRepository,
|
||||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
|
@ -69,8 +69,6 @@ internal class MarketsListModel @Inject constructor(
|
||||||
visibleItemsChanged = { visibleItemIds.value = it },
|
visibleItemsChanged = { visibleItemIds.value = it },
|
||||||
onRetryButtonClicked = { activeListManager.reload() },
|
onRetryButtonClicked = { activeListManager.reload() },
|
||||||
onTokenClick = { onTokenUIClicked(it) },
|
onTokenClick = { onTokenUIClicked(it) },
|
||||||
onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) },
|
|
||||||
onStakingNotificationCloseClick = { onStakingNotificationCloseClick() },
|
|
||||||
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
|
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -112,53 +110,62 @@ internal class MarketsListModel @Inject constructor(
|
||||||
marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode ->
|
marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode ->
|
||||||
if (isInSearchMode) {
|
if (isInSearchMode) {
|
||||||
combine(
|
combine(
|
||||||
searchMarketsListManager.uiItems,
|
flow = searchMarketsListManager.uiItems,
|
||||||
searchMarketsListManager.isInInitialLoadingErrorState,
|
flow2 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||||
searchMarketsListManager.isSearchNotFoundState,
|
flow3 = searchMarketsListManager.isSearchNotFoundState,
|
||||||
getStakingNotificationMaxApyUseCase(),
|
flow4 = shouldShowYieldModeMarketPromoUseCase(
|
||||||
getUserCountryUseCase.invoke(),
|
appCurrency = currentAppCurrency.value,
|
||||||
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry ->
|
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||||
|
),
|
||||||
|
flow5 = getUserCountryUseCase.invoke(),
|
||||||
|
) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo, userCountry ->
|
||||||
MarketsItemsData(
|
MarketsItemsData(
|
||||||
items = uiItems,
|
items = uiItems,
|
||||||
isInErrorState = isInInitialLoadingErrorState,
|
isInErrorState = isInInitialLoadingErrorState,
|
||||||
isSearchNotFound = isSearchNotFoundState,
|
isSearchNotFound = isSearchNotFoundState,
|
||||||
stakingNotificationMaxApy = stakingMaxApy,
|
shouldShowYieldModePromo = isYieldModePromo,
|
||||||
userCountry = userCountry,
|
userCountry = userCountry,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
combine(
|
combine(
|
||||||
mainMarketsListManager.uiItems,
|
flow = mainMarketsListManager.uiItems,
|
||||||
mainMarketsListManager.isInInitialLoadingErrorState,
|
flow2 = mainMarketsListManager.isInInitialLoadingErrorState,
|
||||||
getStakingNotificationMaxApyUseCase(),
|
flow3 = shouldShowYieldModeMarketPromoUseCase(
|
||||||
getUserCountryUseCase.invoke(),
|
appCurrency = currentAppCurrency.value,
|
||||||
) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry ->
|
interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(),
|
||||||
|
),
|
||||||
|
flow4 = getUserCountryUseCase.invoke(),
|
||||||
|
) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo, userCountry ->
|
||||||
MarketsItemsData(
|
MarketsItemsData(
|
||||||
items = uiItems,
|
items = uiItems,
|
||||||
isInErrorState = isInInitialLoadingErrorState,
|
isInErrorState = isInInitialLoadingErrorState,
|
||||||
isSearchNotFound = false,
|
isSearchNotFound = false,
|
||||||
stakingNotificationMaxApy = stakingNotificationMaxApy,
|
shouldShowYieldModePromo = shouldShowYieldModePromo,
|
||||||
userCountry = userCountry,
|
userCountry = userCountry,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.collect { marketsItemsData ->
|
}.collect { marketsItemsData ->
|
||||||
val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless {
|
val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo
|
||||||
marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions()
|
if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) {
|
||||||
}
|
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown())
|
||||||
|
|
||||||
if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null &&
|
|
||||||
stakingNotificationMaxApy != null
|
|
||||||
) {
|
|
||||||
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
marketsListUMStateManager.onUiItemsChanged(
|
marketsListUMStateManager.onUiItemsChanged(
|
||||||
uiItems = marketsItemsData.items,
|
uiItems = marketsItemsData.items,
|
||||||
isInErrorState = marketsItemsData.isInErrorState,
|
isInErrorState = marketsItemsData.isInErrorState,
|
||||||
isSearchNotFound = marketsItemsData.isSearchNotFound,
|
isSearchNotFound = marketsItemsData.isSearchNotFound,
|
||||||
stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless {
|
marketsNotificationUM = if (shouldShowYieldModePromo) {
|
||||||
marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions()
|
MarketsNotificationUM.YieldSupplyPromo(
|
||||||
|
onClick = {
|
||||||
|
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModeMoreInfoClicked())
|
||||||
|
marketsListUMStateManager.selectedSortByType = SortByTypeUM.YieldSupply
|
||||||
|
},
|
||||||
|
onCloseClick = { onYieldModeNotificationCloseClick() },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -266,6 +273,14 @@ internal class MarketsListModel @Inject constructor(
|
||||||
mainMarketsListManager.reload()
|
mainMarketsListManager.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval {
|
||||||
|
return when (this) {
|
||||||
|
TrendInterval.H24 -> TokenMarketListConfig.Interval.H24
|
||||||
|
TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK
|
||||||
|
TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initAnalytics() {
|
private fun initAnalytics() {
|
||||||
containerBottomSheetState.onEach { bottomSheetState ->
|
containerBottomSheetState.onEach { bottomSheetState ->
|
||||||
if (bottomSheetState == BottomSheetState.EXPANDED) {
|
if (bottomSheetState == BottomSheetState.EXPANDED) {
|
||||||
|
|
@ -302,10 +317,10 @@ internal class MarketsListModel @Inject constructor(
|
||||||
}.saveIn(updateQuotesJob)
|
}.saveIn(updateQuotesJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStakingNotificationCloseClick() {
|
private fun onYieldModeNotificationCloseClick() {
|
||||||
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed())
|
analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoClosed())
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
promoRepository.setMarketsStakingNotificationHideClicked()
|
promoRepository.setMarketsYieldSupplyNotificationHideClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,7 +328,7 @@ internal class MarketsListModel @Inject constructor(
|
||||||
val items: ImmutableList<MarketsListItemUM>,
|
val items: ImmutableList<MarketsListItemUM>,
|
||||||
val isInErrorState: Boolean,
|
val isInErrorState: Boolean,
|
||||||
val isSearchNotFound: Boolean,
|
val isSearchNotFound: Boolean,
|
||||||
val stakingNotificationMaxApy: BigDecimal?,
|
val shouldShowYieldModePromo: Boolean,
|
||||||
val userCountry: Either<UserCountryError, UserCountry>,
|
val userCountry: Either<UserCountryError, UserCountry>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.tangem.features.markets.tokenlist.impl.model
|
||||||
|
|
||||||
|
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.markets.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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter(
|
||||||
trendType = value.getTrendType(),
|
trendType = value.getTrendType(),
|
||||||
chartData = value.getChartData(),
|
chartData = value.getChartData(),
|
||||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||||
stakingRate = value.stakingRate?.format { percent() }?.let {
|
stakingRate = value.yieldRate?.format { percent() }?.let {
|
||||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||||
},
|
},
|
||||||
updateTimestamp = value.updateTimestamp,
|
updateTimestamp = value.updateTimestamp,
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,7 @@ internal class MarketsListBatchFlowManager(
|
||||||
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
||||||
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
||||||
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
||||||
|
SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.event.triggeredEvent
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.features.markets.impl.R
|
import com.tangem.features.markets.impl.R
|
||||||
|
import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.*
|
import com.tangem.features.markets.tokenlist.impl.ui.state.*
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -26,8 +26,6 @@ internal class MarketsListUMStateManager(
|
||||||
private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> Unit,
|
private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> Unit,
|
||||||
private val onRetryButtonClicked: () -> Unit,
|
private val onRetryButtonClicked: () -> Unit,
|
||||||
private val onTokenClick: (MarketsListItemUM) -> Unit,
|
private val onTokenClick: (MarketsListItemUM) -> Unit,
|
||||||
private val onStakingNotificationClick: () -> Unit,
|
|
||||||
private val onStakingNotificationCloseClick: () -> Unit,
|
|
||||||
private val onShowTokensUnder100kClicked: () -> Unit,
|
private val onShowTokensUnder100kClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -92,25 +90,25 @@ internal class MarketsListUMStateManager(
|
||||||
isInErrorState: Boolean,
|
isInErrorState: Boolean,
|
||||||
isSearchNotFound: Boolean,
|
isSearchNotFound: Boolean,
|
||||||
uiItems: ImmutableList<MarketsListItemUM>,
|
uiItems: ImmutableList<MarketsListItemUM>,
|
||||||
stakingNotificationMaxApy: BigDecimal?,
|
marketsNotificationUM: MarketsNotificationUM?,
|
||||||
) {
|
) {
|
||||||
state.update {
|
state.update { currentState ->
|
||||||
when {
|
when {
|
||||||
isInErrorState -> {
|
isInErrorState -> {
|
||||||
it.copy(
|
currentState.copy(
|
||||||
list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked),
|
list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isSearchNotFound -> {
|
isSearchNotFound -> {
|
||||||
it.copy(list = ListUM.SearchNothingFound)
|
currentState.copy(list = ListUM.SearchNothingFound)
|
||||||
}
|
}
|
||||||
uiItems.isEmpty() -> {
|
uiItems.isEmpty() -> {
|
||||||
it.copy(list = ListUM.Loading)
|
currentState.copy(list = ListUM.Loading)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
it.updateItems(
|
currentState.updateItems(
|
||||||
newItems = uiItems,
|
newItems = uiItems,
|
||||||
stakingNotificationMaxApy = stakingNotificationMaxApy,
|
marketsNotificationUM = marketsNotificationUM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +117,7 @@ internal class MarketsListUMStateManager(
|
||||||
|
|
||||||
private fun MarketsListUM.updateItems(
|
private fun MarketsListUM.updateItems(
|
||||||
newItems: ImmutableList<MarketsListItemUM>,
|
newItems: ImmutableList<MarketsListItemUM>,
|
||||||
stakingNotificationMaxApy: BigDecimal?,
|
marketsNotificationUM: MarketsNotificationUM?,
|
||||||
): MarketsListUM {
|
): MarketsListUM {
|
||||||
val currentState = this
|
val currentState = this
|
||||||
|
|
||||||
|
|
@ -131,7 +129,7 @@ internal class MarketsListUMStateManager(
|
||||||
.copy(
|
.copy(
|
||||||
showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(),
|
showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(),
|
||||||
),
|
),
|
||||||
stakingNotificationMaxApy = stakingNotificationMaxApy,
|
marketsNotificationUM = marketsNotificationUM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,12 +223,7 @@ internal class MarketsListUMStateManager(
|
||||||
onOptionClicked = ::onBottomSheetOptionClicked,
|
onOptionClicked = ::onBottomSheetOptionClicked,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
stakingNotificationMaxApy = null,
|
marketsNotificationUM = null,
|
||||||
onStakingNotificationClick = {
|
|
||||||
onStakingNotificationClick()
|
|
||||||
selectedSortByType = SortByTypeUM.Staking
|
|
||||||
},
|
|
||||||
onStakingNotificationCloseClick = onStakingNotificationCloseClick,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {
|
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -38,19 +37,17 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||||
import com.tangem.core.ui.components.fields.SearchBar
|
import com.tangem.core.ui.components.fields.SearchBar
|
||||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||||
import com.tangem.core.ui.components.keyboardAsState
|
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.event.consumedEvent
|
||||||
import com.tangem.core.ui.extensions.*
|
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.LocalMainBottomSheetColor
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.features.markets.impl.R
|
import com.tangem.features.markets.impl.R
|
||||||
|
import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
|
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.MarketsListSortByBottomSheet
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification
|
import com.tangem.features.markets.tokenlist.impl.ui.components.YieldSupplyInMarketsPromoNotification
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
|
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.ListUM
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
|
||||||
|
|
@ -58,7 +55,6 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont
|
||||||
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
private const val SHOW_MORE_KEY = "privacyPolicy"
|
private const val SHOW_MORE_KEY = "privacyPolicy"
|
||||||
|
|
||||||
|
|
@ -180,18 +176,21 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val marketsNotification = state.marketsNotificationUM
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
state.isInSearchMode.not() &&
|
state.isInSearchMode.not() &&
|
||||||
state.stakingNotificationMaxApy != null &&
|
state.selectedSortBy != SortByTypeUM.YieldSupply,
|
||||||
state.selectedSortBy != SortByTypeUM.Staking,
|
|
||||||
) {
|
) {
|
||||||
val showMore = stringResourceSafe(R.string.common_show_more)
|
val showMore = stringResourceSafe(R.string.common_show_more)
|
||||||
|
|
||||||
|
when (marketsNotification) {
|
||||||
|
is MarketsNotificationUM.YieldSupplyPromo -> {
|
||||||
val description = stringResourceSafe(
|
val description = stringResourceSafe(
|
||||||
R.string.markets_staking_banner_description_placeholder,
|
R.string.markets_yield_supply_banner_description,
|
||||||
showMore,
|
showMore,
|
||||||
)
|
)
|
||||||
|
|
||||||
val clickableDescription = buildAnnotatedString {
|
val clickableDescription = annotatedReference {
|
||||||
append(description.substringBefore(showMore))
|
append(description.substringBefore(showMore))
|
||||||
|
|
||||||
pushStringAnnotation(SHOW_MORE_KEY, "")
|
pushStringAnnotation(SHOW_MORE_KEY, "")
|
||||||
|
|
@ -199,20 +198,17 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
StakingInMarketsPromoNotification(
|
YieldSupplyInMarketsPromoNotification(
|
||||||
config = NotificationConfig(
|
config = marketsNotification.config.copy(
|
||||||
iconResId = R.drawable.img_staking_in_market_notification,
|
subtitle = clickableDescription,
|
||||||
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),
|
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> { /* no-op */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val strokeWidth = TangemTheme.dimens.size0_5
|
val strokeWidth = TangemTheme.dimens.size0_5
|
||||||
|
|
@ -420,9 +416,10 @@ private fun Preview() {
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||||
),
|
),
|
||||||
stakingNotificationMaxApy = BigDecimal(0.12345),
|
marketsNotificationUM = MarketsNotificationUM.YieldSupplyPromo(
|
||||||
onStakingNotificationClick = {},
|
onClick = {},
|
||||||
onStakingNotificationCloseClick = {},
|
onCloseClick = {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onHeaderSizeChange = {},
|
onHeaderSizeChange = {},
|
||||||
bottomSheetState = BottomSheetState.EXPANDED,
|
bottomSheetState = BottomSheetState.EXPANDED,
|
||||||
|
|
|
||||||
|
|
@ -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(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
|
||||||
|
|
@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.features.markets.impl.R
|
import com.tangem.features.markets.impl.R
|
||||||
|
import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
internal data class MarketsListUM(
|
internal data class MarketsListUM(
|
||||||
val list: ListUM,
|
val list: ListUM,
|
||||||
|
|
@ -19,9 +19,7 @@ internal data class MarketsListUM(
|
||||||
val selectedInterval: TrendInterval,
|
val selectedInterval: TrendInterval,
|
||||||
val onIntervalClick: (TrendInterval) -> Unit,
|
val onIntervalClick: (TrendInterval) -> Unit,
|
||||||
val onSortByButtonClick: () -> Unit,
|
val onSortByButtonClick: () -> Unit,
|
||||||
val stakingNotificationMaxApy: BigDecimal?,
|
val marketsNotificationUM: MarketsNotificationUM?,
|
||||||
val onStakingNotificationClick: () -> Unit,
|
|
||||||
val onStakingNotificationCloseClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val isInSearchMode
|
val isInSearchMode
|
||||||
get() = searchBar.isActive
|
get() = searchBar.isActive
|
||||||
|
|
@ -40,6 +38,7 @@ enum class SortByTypeUM(val text: TextReference) {
|
||||||
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
||||||
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
||||||
Staking(resourceReference(R.string.common_staking)),
|
Staking(resourceReference(R.string.common_staking)),
|
||||||
|
YieldSupply(resourceReference(R.string.yield_module_earn_sheet_title)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue