Updated on 2026-08-14

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

View file

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

View file

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

View file

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

View file

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

View file

@ -86,6 +86,12 @@ object PreferencesKeys {
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } val 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 IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") }

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

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

View file

@ -27,7 +27,9 @@ import com.tangem.domain.wallets.models.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 java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
@ -41,6 +43,7 @@ internal class DefaultMarketsTokenRepository(
private val excludedBlockchains: ExcludedBlockchains, private val excludedBlockchains: ExcludedBlockchains,
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?>,
) : MarketsTokenRepository { ) : MarketsTokenRepository {
private val tokenMarketInfoConverter: TokenMarketInfoConverter = TokenMarketInfoConverter(excludedBlockchains) private val tokenMarketInfoConverter: TokenMarketInfoConverter = TokenMarketInfoConverter(excludedBlockchains)
@ -88,8 +91,12 @@ internal class DefaultMarketsTokenRepository(
val last = res.tokens.size < request.limit val last = res.tokens.size < request.limit
val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res)
maxApyStore.store(tokenMarketListWithMaxApy.maxApy)
return BatchFetchResult.Success( return BatchFetchResult.Success(
data = TokenMarketListConverter.convert(res), data = tokenMarketListWithMaxApy.tokens,
last = last, last = last,
empty = res.tokens.isEmpty(), 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 { inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
return catchErrorAndSendEvent(block, ::createListErrorEvent) return catchErrorAndSendEvent(block, ::createListErrorEvent)
} }

View file

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

View file

@ -2,22 +2,29 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListWithMaxApy
import com.tangem.domain.markets.TokenQuotesShort import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter 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 { val imageHost = value.imageHost ?: run {
if (value.tokens.isEmpty()) { if (value.tokens.isEmpty()) {
return emptyList() return TokenMarketListWithMaxApy(emptyList(), null)
} else { } else {
error("imageHost cannot be null") 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( TokenMarket(
id = CryptoCurrency.RawID(token.id), id = CryptoCurrency.RawID(token.id),
name = token.name, name = token.name,
@ -33,7 +40,9 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, Li
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,
) )
} }
return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy)
} }
} }

View file

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

View file

@ -67,6 +67,20 @@ internal class DefaultPromoRepository(
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false) 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 { override fun getStoryById(id: String): Flow<StoryContent?> = isReadyToShowStories(id).mapLatest {
getStoryByIdSync(id = id, refresh = false) getStoryByIdSync(id = id, refresh = false)
} }

View file

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

View file

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

View file

@ -12,6 +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?,
private val imageHost: String, private val imageHost: String,
) { ) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,8 @@ import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.ui.components.marketprice.PriceChangeType 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.compact
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format 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.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket 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.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
@ -37,8 +40,11 @@ internal class MarketsTokenItemConverter(
price = value.getCurrentPrice(), price = value.getCurrentPrice(),
trendPercentText = value.getTrendPercent(), trendPercentText = value.getTrendPercent(),
trendType = value.getTrendType(), trendType = value.getTrendType(),
chardData = value.getChartData(), chartData = value.getChartData(),
isUnder100kMarketCap = value.isUnderMarketCapLimit, 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, prevUI.trendPercentText,
) { new.getTrendPercent() }, ) { new.getTrendPercent() },
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() }, chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
) )
} }

View file

@ -323,6 +323,7 @@ internal class MarketsListBatchFlowManager(
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
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
} }
} }

View file

@ -16,14 +16,18 @@ 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")
internal class MarketsListUMStateManager( internal class MarketsListUMStateManager(
private val currentVisibleIds: Provider<List<CryptoCurrency.RawID>>, private val currentVisibleIds: Provider<List<CryptoCurrency.RawID>>,
private val onLoadMoreUiItems: () -> Unit, private val onLoadMoreUiItems: () -> Unit,
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 var sortByBottomSheetIsShown private var sortByBottomSheetIsShown
@ -87,6 +91,7 @@ internal class MarketsListUMStateManager(
isInErrorState: Boolean, isInErrorState: Boolean,
isSearchNotFound: Boolean, isSearchNotFound: Boolean,
uiItems: ImmutableList<MarketsListItemUM>, uiItems: ImmutableList<MarketsListItemUM>,
stakingNotificationMaxApy: BigDecimal?,
) { ) {
state.update { state.update {
when { when {
@ -102,13 +107,19 @@ internal class MarketsListUMStateManager(
it.copy(list = ListUM.Loading) it.copy(list = ListUM.Loading)
} }
else -> { 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 val currentState = this
if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) { if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) {
@ -119,6 +130,7 @@ internal class MarketsListUMStateManager(
.copy( .copy(
showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(),
), ),
stakingNotificationMaxApy = stakingNotificationMaxApy,
) )
} }
@ -211,6 +223,12 @@ internal class MarketsListUMStateManager(
onOptionClicked = ::onBottomSheetOptionClicked, onOptionClicked = ::onBottomSheetOptionClicked,
), ),
), ),
stakingNotificationMaxApy = null,
onStakingNotificationClick = {
onStakingNotificationClick()
selectedSortByType = SortByTypeUM.Staking
},
onStakingNotificationCloseClick = onStakingNotificationCloseClick,
) )
private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) {

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.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
import androidx.compose.ui.text.buildAnnotatedString
import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8 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.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.resolveReference import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.extensions.stringResourceSafe 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
@ -41,6 +43,7 @@ import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R 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.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.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
@ -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 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"
@Composable @Composable
internal fun MarketsList( internal fun MarketsList(
@ -110,6 +116,7 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
SpacerH12() SpacerH12()
} }
} }
Column {
AnimatedVisibility(state.isInSearchMode.not()) { AnimatedVisibility(state.isInSearchMode.not()) {
Options( Options(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
@ -119,6 +126,41 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
onSortByClick = state.onSortByButtonClick, 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 val strokeWidth = TangemTheme.dimens.size0_5
Box( Box(
@ -326,6 +368,9 @@ private fun Preview() {
onDismissRequest = {}, onDismissRequest = {},
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
), ),
stakingNotificationMaxApy = BigDecimal(0.12345),
onStakingNotificationClick = {},
onStakingNotificationCloseClick = {},
), ),
onHeaderSizeChange = {}, onHeaderSizeChange = {},
bottomSheetState = BottomSheetState.EXPANDED, bottomSheetState = BottomSheetState.EXPANDED,

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.components
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button 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.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType 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.LocalWindowSize
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
@ -95,6 +98,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier
.alignByBaseline(), .alignByBaseline(),
ratingPosition = model.ratingPosition, ratingPosition = model.ratingPosition,
marketCap = model.marketCap, marketCap = model.marketCap,
stakingRate = model.stakingRate,
) )
PriceChangeInPercent( PriceChangeInPercent(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
@ -110,7 +114,7 @@ private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier
Chart( Chart(
chartType = model.chartType, chartType = model.chartType,
chartRawData = model.chardData, chartRawData = model.chartData,
) )
} }
} }
@ -142,7 +146,12 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier
} }
@Composable @Composable
private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) { private fun TokenSubtitle(
ratingPosition: String?,
marketCap: String?,
stakingRate: TextReference?,
modifier: Modifier = Modifier,
) {
Row( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -150,6 +159,10 @@ private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier:
TokenRatingPlace(ratingPosition = ratingPosition) TokenRatingPlace(ratingPosition = ratingPosition)
SpacerW4() SpacerW4()
TokenMarketCapText(text = marketCap ?: "") 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 @Composable
private fun RowScope.TokenMarketCapText(text: String) { private fun RowScope.TokenMarketCapText(text: String) {
Text( Text(

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
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,
@ -18,6 +19,9 @@ 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 onStakingNotificationClick: () -> Unit,
val onStakingNotificationCloseClick: () -> Unit,
) { ) {
val isInSearchMode val isInSearchMode
get() = searchBar.isActive get() = searchBar.isActive
@ -35,6 +39,7 @@ enum class SortByTypeUM(val text: TextReference) {
ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)), ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)),
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)),
} }
@Immutable @Immutable

View file

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