From a72837204da2b0799da5afc1cd7f29b5ef7b21c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:06:35 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 16 ++ .../tap/di/domain/SettingsDomainModule.kt | 8 + .../com/tangem/tap/features/home/HomeModel.kt | 2 +- .../response/TokenMarketListResponse.kt | 24 ++ .../local/preferences/PreferencesKeys.kt | 6 + .../components/notifications/Notification.kt | 2 +- .../img_staking_in_market_notification.webp | Bin 0 -> 15688 bytes data/markets/build.gradle.kts | 1 + .../markets/DefaultMarketsTokenRepository.kt | 13 +- .../converters/TokenListConfigConverters.kt | 1 + .../converters/TokenMarketListConverter.kt | 17 +- .../data/markets/di/MarketsDataModule.kt | 3 +- .../data/promo/DefaultPromoRepository.kt | 14 ++ .../settings/DefaultSettingsRepository.kt | 11 + domain/markets/build.gradle.kts | 2 + .../com/tangem/domain/markets/TokenMarket.kt | 1 + .../domain/markets/TokenMarketListConfig.kt | 2 +- .../markets/TokenMarketListWithMaxApy.kt | 8 + .../GetStakingNotificationMaxApyUseCase.kt | 39 +++ .../repositories/MarketsTokenRepository.kt | 4 + .../tangem/domain/promo/PromoRepository.kt | 4 + .../SetWalletFirstTimeUsageUseCase.kt | 16 ++ .../repositories/SettingsRepository.kt | 4 + .../analytics/MarketsListAnalyticsEvent.kt | 7 + .../tokenlist/impl/model/MarketsListModel.kt | 233 ++++++++++-------- .../converters/MarketsTokenItemConverter.kt | 10 +- .../MarketsListBatchFlowManager.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 22 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 67 ++++- .../impl/ui/components/MarketsListItem.kt | 39 ++- .../StakingInMarketsPromoNotification.kt | 147 +++++++++++ .../MarketChartListItemPreviewDataProvider.kt | 19 +- .../impl/ui/state/MarketsListItemUM.kt | 4 +- .../tokenlist/impl/ui/state/MarketsListUM.kt | 5 + .../wallet/child/wallet/model/WalletModel.kt | 8 + 35 files changed, 628 insertions(+), 132 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_staking_in_market_notification.webp create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 7666729a36..4889626ff1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 7dc5634128..df25dd1255 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 91c45b832a..a3b184dc8e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index b2af512cda..3c1829900b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -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?, ) { @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?, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 90b07d055f..5e4f9577d1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 4050ff267f..a22a0c468c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -321,7 +321,7 @@ private fun SecondaryPairButtons( } @Composable -internal fun CloseableIconButton( +fun CloseableIconButton( onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true, diff --git a/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp b/core/ui/src/main/res/drawable/img_staking_in_market_notification.webp new file mode 100644 index 0000000000000000000000000000000000000000..de2b304a0f2fe8eaf61d490c5108bcbb91380b91 GIT binary patch literal 15688 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 literal 0 HcmV?d00001 diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index fa746e853f..dec987eadb 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { // endregion // region Others dependencies + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.jodatime) implementation(deps.moshi) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 91c229e3ef..3eb641be45 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -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>, + private val maxApyStore: RuntimeStateStore, ) : 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 { + return maxApyStore.get() + } + inline fun catchListErrorAndSendEvent(block: () -> T): T { return catchErrorAndSendEvent(block, ::createListErrorEvent) } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index 1875a5b609..e280e160e1 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -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) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 2091fcf399..da25a28d09 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -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> { +internal object TokenMarketListConverter : Converter { - override fun convert(value: TokenMarketListResponse): List { + 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 { + 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 = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 87ea3f6639..8bc077f7c7 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -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, diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 7d820cb19a..735657a25b 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -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) diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index 1724d9ce45..d3c97c6943 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -12,6 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, + val stakingRate: BigDecimal?, private val imageHost: String, ) { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index c3633f3925..011df2c630 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -8,7 +8,7 @@ data class TokenMarketListConfig( ) { enum class Order { - ByRating, Trending, Buyers, TopGainers, TopLosers + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking } enum class Interval { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt new file mode 100644 index 0000000000..3b3bc77831 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListWithMaxApy.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarketListWithMaxApy( + val tokens: List, + val maxApy: BigDecimal?, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt new file mode 100644 index 0000000000..a14d6c7cbd --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 80a1a97480..c0fe937411 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -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 + + suspend fun getMaxApy(): Flow } \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 88682deae2..bed593a152 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -15,6 +15,10 @@ interface PromoRepository { suspend fun setNeverToShowWalletPromo(promoId: PromoId) suspend fun setNeverToShowTokenPromo(promoId: PromoId) + + suspend fun isMarketsStakingNotificationHideClicked(): Flow + + suspend fun setMarketsStakingNotificationHideClicked() // endregion // region Stories diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt new file mode 100644 index 0000000000..c0cef31d77 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetWalletFirstTimeUsageUseCase.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 23b4a1756c..1a64989894 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -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) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt index 99f0faa3c3..3395db941b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -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") } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 65622d2f0d..c3c73fed42 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.tokenlist.impl.model import androidx.compose.runtime.Stable +import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -8,7 +9,13 @@ import com.tangem.core.decompose.model.Model import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.UserCountryError +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent @@ -21,8 +28,10 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import java.math.BigDecimal import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -31,23 +40,26 @@ private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @ModelScoped @Stable +@Suppress("LongParameterList") internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + private val promoRepository: PromoRepository, + private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private var updateQuotesJob = JobHolder() - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) private val visibleItemIds = MutableStateFlow>(emptyList()) @@ -57,7 +69,10 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, + onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, + onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, ) + private val mainMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -92,31 +107,60 @@ internal class MarketsListModel @Inject constructor( val state = marketsListUMStateManager.state.asStateFlow() init { - @Suppress("UnnecessaryParentheses") - modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .flatMapLatest { isInSearchMode -> - if (isInSearchMode) { - combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - ) { items, isError, notFound -> - (items to isError) to notFound - } - } else { - combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - ) { items, isError -> (items to isError) to false } + @Suppress("UnnecessaryParentheses") modelScope.launch { + marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> + if (isInSearchMode) { + combine( + searchMarketsListManager.uiItems, + searchMarketsListManager.isInInitialLoadingErrorState, + searchMarketsListManager.isSearchNotFoundState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = isSearchNotFoundState, + stakingNotificationMaxApy = stakingMaxApy, + userCountry = userCountry, + ) + } + } else { + combine( + mainMarketsListManager.uiItems, + mainMarketsListManager.isInInitialLoadingErrorState, + getStakingNotificationMaxApyUseCase(), + getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + MarketsItemsData( + items = uiItems, + isInErrorState = isInInitialLoadingErrorState, + isSearchNotFound = false, + stakingNotificationMaxApy = stakingNotificationMaxApy, + userCountry = userCountry, + ) } - }.collect { - marketsListUMStateManager.onUiItemsChanged( - uiItems = it.first.first, - isInErrorState = it.first.second, - isSearchNotFound = it.second, - ) } + }.collect { marketsItemsData -> + val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + } + + if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && + stakingNotificationMaxApy != null + ) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + } + + marketsListUMStateManager.onUiItemsChanged( + uiItems = marketsItemsData.items, + isInErrorState = marketsItemsData.isInErrorState, + isSearchNotFound = marketsItemsData.isSearchNotFound, + stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { + marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + }, + ) + } } state.onEach { @@ -126,29 +170,22 @@ internal class MarketsListModel @Inject constructor( }.launchIn(modelScope) // update all lists when user's currency has changed - currentAppCurrency - .drop(1) - .onEach { - mainMarketsListManager.reload() - if (marketsListUMStateManager.isInSearchState) { - searchMarketsListManager.reload() - } - }.launchIn(modelScope) + currentAppCurrency.drop(1).onEach { + mainMarketsListManager.reload() + if (marketsListUMStateManager.isInSearchState) { + searchMarketsListManager.reload() + } + }.launchIn(modelScope) // load charts when new batch is being loaded - mainMarketsListManager.onLastBatchLoadedSuccess - .onEach { - mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } - .launchIn(modelScope) + mainMarketsListManager.onLastBatchLoadedSuccess.onEach { + mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + }.launchIn(modelScope) // listen currently selected interval, update charts if sorting=rating, or reload all list modelScope.launch(dispatchers.default) { - marketsListUMStateManager.state - .map { it.selectedInterval } - .distinctUntilChanged() - .drop(1) + marketsListUMStateManager.state.map { it.selectedInterval }.distinctUntilChanged().drop(1) .collectLatest { interval -> when (marketsListUMStateManager.selectedSortByType) { SortByTypeUM.Rating -> { @@ -163,66 +200,52 @@ internal class MarketsListModel @Inject constructor( // reload list when sorting type has changed modelScope.launch { - marketsListUMStateManager.state - .map { it.selectedSortBy } - .distinctUntilChanged() - .drop(1) - .collectLatest { - mainMarketsListManager.reload() - } + marketsListUMStateManager.state.map { it.selectedSortBy }.distinctUntilChanged().drop(1).collectLatest { + mainMarketsListManager.reload() + } } // listen current visible batch and update charts modelScope.launch { - visibleItemIds - .mapNotNull { - if (it.isNotEmpty()) { - activeListManager.getBatchKeysByItemIds(visibleItemIds.value) - } else { - null - } - } - .distinctUntilChanged() - .collectLatest { visibleBatchKeys -> - // TODO load batch on scroll heat area - activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + visibleItemIds.mapNotNull { + if (it.isNotEmpty()) { + activeListManager.getBatchKeysByItemIds(visibleItemIds.value) + } else { + null } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + // TODO load batch on scroll heat area + activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + } } // ===Search=== modelScope.launch { - marketsListUMStateManager.isInSearchStateFlow - .collectLatest { isInSearchMode -> - activeListManager = if (isInSearchMode) { - searchMarketsListManager - } else { - searchMarketsListManager.clearStateAndStopAllActions() - mainMarketsListManager - } + marketsListUMStateManager.isInSearchStateFlow.collectLatest { isInSearchMode -> + activeListManager = if (isInSearchMode) { + searchMarketsListManager + } else { + searchMarketsListManager.clearStateAndStopAllActions() + mainMarketsListManager } + } } modelScope.launch { - marketsListUMStateManager.searchQueryFlow - .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) - .distinctUntilChanged() - .onEach { + marketsListUMStateManager.searchQueryFlow.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) + .distinctUntilChanged().onEach { if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions() - } - .filter { it.isNotEmpty() && activeListManager == searchMarketsListManager } - .collectLatest { + }.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }.collectLatest { searchMarketsListManager.reload(searchText = it) } } modelScope.launch { - searchMarketsListManager - .onLastBatchLoadedSuccess - .collectLatest { - searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) - modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) - } + searchMarketsListManager.onLastBatchLoadedSuccess.collectLatest { + searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + } } // analytics @@ -233,17 +256,14 @@ internal class MarketsListModel @Inject constructor( } private fun initAnalytics() { - containerBottomSheetState - .onEach { - if (it == BottomSheetState.EXPANDED) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) - } - }.launchIn(modelScope) + containerBottomSheetState.onEach { + if (it == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + } + }.launchIn(modelScope) - state - .filter { it.isInSearchMode.not() } - .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) } - .distinctUntilChanged() + state.filter { it.isInSearchMode.not() } + .map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }.distinctUntilChanged() .onEach { analyticsEventHandler.send(it) }.launchIn(modelScope) @@ -270,4 +290,19 @@ internal class MarketsListModel @Inject constructor( } }.saveIn(updateQuotesJob) } + + private fun onStakingNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + modelScope.launch { + promoRepository.setMarketsStakingNotificationHideClicked() + } + } + + private class MarketsItemsData( + val items: ImmutableList, + val isInErrorState: Boolean, + val isSearchNotFound: Boolean, + val stakingNotificationMaxApy: BigDecimal?, + val userCountry: Either, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c4adb37979..d7352826b5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -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() }, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index e178484e78..3cae93db38 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -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 } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index aa7f006d19..ac022dcd2a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -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>, private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> 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, + 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): MarketsListUM { + private fun MarketsListUM.updateItems( + newItems: ImmutableList, + 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) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index e1b6bfd37b..1442d9643f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.buildAnnotatedString import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 @@ -29,10 +30,11 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -41,6 +43,7 @@ import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet +import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM @@ -48,6 +51,9 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +private const val SHOW_MORE_KEY = "privacyPolicy" @Composable internal fun MarketsList( @@ -110,14 +116,50 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi SpacerH12() } } - AnimatedVisibility(state.isInSearchMode.not()) { - Options( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) + Column { + AnimatedVisibility(state.isInSearchMode.not()) { + Options( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + + AnimatedVisibility( + state.isInSearchMode.not() && + state.stakingNotificationMaxApy != null && + state.selectedSortBy != SortByTypeUM.Staking, + ) { + val showMore = stringResourceSafe(R.string.markets_staking_banner_description_show_more) + val description = stringResourceSafe( + R.string.markets_staking_banner_description_placeholder, + showMore, + ) + + val clickableDescription = buildAnnotatedString { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + StakingInMarketsPromoNotification( + config = NotificationConfig( + iconResId = R.drawable.img_staking_in_market_notification, + title = resourceReference( + R.string.markets_staking_banner_title, + wrappedList(state.stakingNotificationMaxApy.format { percent() }), + ), + subtitle = annotatedReference(clickableDescription), + onClick = state.onStakingNotificationClick, + onCloseClick = state.onStakingNotificationCloseClick, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) + } } } val strokeWidth = TangemTheme.dimens.size0_5 @@ -326,6 +368,9 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), + stakingNotificationMaxApy = BigDecimal(0.12345), + onStakingNotificationClick = {}, + onStakingNotificationCloseClick = {}, ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index c153024da0..4c8ed29713 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -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( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt new file mode 100644 index 0000000000..9026a0ebb4 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/StakingInMarketsPromoNotification.kt @@ -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 \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 97eecee6d1..464335eb3b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -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%"), ), ), ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 458a1e29a3..2cb553987a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -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 diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index 298f90f22f..2e99a45aec 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 4922cf66c1..b06d42aa58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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()