Updated on 2026-08-14
This commit is contained in:
parent
8009743fb9
commit
a72837204d
35 changed files with 628 additions and 132 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.moshi)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ data class TokenMarket(
|
|||
val isUnderMarketCapLimit: Boolean,
|
||||
val tokenQuotesShort: TokenQuotesShort,
|
||||
val tokenCharts: Charts,
|
||||
val stakingRate: BigDecimal?,
|
||||
private val imageHost: String,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ data class TokenMarketListConfig(
|
|||
) {
|
||||
|
||||
enum class Order {
|
||||
ByRating, Trending, Buyers, TopGainers, TopLosers
|
||||
ByRating, Trending, Buyers, TopGainers, TopLosers, Staking
|
||||
}
|
||||
|
||||
enum class Interval {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TokenMarketListWithMaxApy(
|
||||
val tokens: List<TokenMarket>,
|
||||
val maxApy: BigDecimal?,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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?>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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,17 +40,20 @@ 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 ->
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
|
|
@ -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,29 +107,58 @@ internal class MarketsListModel @Inject constructor(
|
|||
val state = marketsListUMStateManager.state.asStateFlow()
|
||||
|
||||
init {
|
||||
@Suppress("UnnecessaryParentheses")
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.isInSearchStateFlow
|
||||
.flatMapLatest { isInSearchMode ->
|
||||
@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
|
||||
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,
|
||||
) { items, isError -> (items to isError) to false }
|
||||
getStakingNotificationMaxApyUseCase(),
|
||||
getUserCountryUseCase.invoke(),
|
||||
) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry ->
|
||||
MarketsItemsData(
|
||||
items = uiItems,
|
||||
isInErrorState = isInInitialLoadingErrorState,
|
||||
isSearchNotFound = false,
|
||||
stakingNotificationMaxApy = stakingNotificationMaxApy,
|
||||
userCountry = userCountry,
|
||||
)
|
||||
}
|
||||
}.collect {
|
||||
}
|
||||
}.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 = it.first.first,
|
||||
isInErrorState = it.first.second,
|
||||
isSearchNotFound = it.second,
|
||||
uiItems = marketsItemsData.items,
|
||||
isInErrorState = marketsItemsData.isInErrorState,
|
||||
isSearchNotFound = marketsItemsData.isSearchNotFound,
|
||||
stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless {
|
||||
marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -126,9 +170,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
}.launchIn(modelScope)
|
||||
|
||||
// update all lists when user's currency has changed
|
||||
currentAppCurrency
|
||||
.drop(1)
|
||||
.onEach {
|
||||
currentAppCurrency.drop(1).onEach {
|
||||
mainMarketsListManager.reload()
|
||||
if (marketsListUMStateManager.isInSearchState) {
|
||||
searchMarketsListManager.reload()
|
||||
|
|
@ -136,19 +178,14 @@ internal class MarketsListModel @Inject constructor(
|
|||
}.launchIn(modelScope)
|
||||
|
||||
// load charts when new batch is being loaded
|
||||
mainMarketsListManager.onLastBatchLoadedSuccess
|
||||
.onEach {
|
||||
mainMarketsListManager.onLastBatchLoadedSuccess.onEach {
|
||||
mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}.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,27 +200,20 @@ internal class MarketsListModel @Inject constructor(
|
|||
|
||||
// reload list when sorting type has changed
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.state
|
||||
.map { it.selectedSortBy }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.collectLatest {
|
||||
marketsListUMStateManager.state.map { it.selectedSortBy }.distinctUntilChanged().drop(1).collectLatest {
|
||||
mainMarketsListManager.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// listen current visible batch and update charts
|
||||
modelScope.launch {
|
||||
visibleItemIds
|
||||
.mapNotNull {
|
||||
visibleItemIds.mapNotNull {
|
||||
if (it.isNotEmpty()) {
|
||||
activeListManager.getBatchKeysByItemIds(visibleItemIds.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { visibleBatchKeys ->
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
// TODO load batch on scroll heat area
|
||||
activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval)
|
||||
}
|
||||
|
|
@ -192,8 +222,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
// ===Search===
|
||||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.isInSearchStateFlow
|
||||
.collectLatest { isInSearchMode ->
|
||||
marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode ->
|
||||
activeListManager = if (isInSearchMode) {
|
||||
searchMarketsListManager
|
||||
} else {
|
||||
|
|
@ -204,22 +233,16 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
|
||||
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.onLastBatchLoadedSuccess.collectLatest {
|
||||
searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
|
||||
}
|
||||
|
|
@ -233,17 +256,14 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initAnalytics() {
|
||||
containerBottomSheetState
|
||||
.onEach {
|
||||
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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,6 +116,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
SpacerH12()
|
||||
}
|
||||
}
|
||||
Column {
|
||||
AnimatedVisibility(state.isInSearchMode.not()) {
|
||||
Options(
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
|
|
@ -119,6 +126,41 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
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
|
||||
Box(
|
||||
|
|
@ -326,6 +368,9 @@ private fun Preview() {
|
|||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
stakingNotificationMaxApy = BigDecimal(0.12345),
|
||||
onStakingNotificationClick = {},
|
||||
onStakingNotificationCloseClick = {},
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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%"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue