From 46c112eca80aefe6b9d57e6dfe21b2e1bbb3e285 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 16:24:18 +0300 Subject: [PATCH 1/4] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e51eb53245..462e99b59c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1327" +tangemBlockchainSdk = "releases-5.32-1329" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-573" +tangemCardSdk = "releases-5.32-574" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From ed89b4bcd0305ffc91899389d9234f0d3a86371f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:10:58 +0500 Subject: [PATCH 2/4] Updated on 2026-08-14 --- .../CardContextInterceptor.kt | 1 + .../HotWalletContextInterceptor.kt | 1 + .../ui/userwallet/UserWalletUnlockError.kt | 11 ++++++-- .../core/analytics/models/event/SignIn.kt | 4 ++- .../details/model/UserWalletListModel.kt | 3 ++- .../stepper/impl/HotWalletStepperModel.kt | 1 - .../intents/WalletWarningsClickIntents.kt | 1 + .../welcome/impl/model/WelcomeModel.kt | 26 ++++++++++++++----- 8 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index cdd42cdde9..5b5902f3cc 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -36,6 +36,7 @@ class CardContextInterceptor( is SignIn.ScreenOpened, is SignIn.ButtonAddWallet, -> false + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll else -> true } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt index f67eaab3f8..e24d5594e5 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt @@ -19,6 +19,7 @@ class HotWalletContextInterceptor( is SignIn.ButtonUnlockAllWithBiometric, is IntroductionProcess.ButtonScanCard, -> false + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll else -> true } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index e7e4b2a884..41fdd9e856 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -16,6 +16,7 @@ inline fun UnlockWalletError.handle( onAlreadyUnlocked: () -> Unit = {}, onUserCancelled: () -> Unit = {}, analyticsEventHandler: AnalyticsEventHandler, + isFromUnlockAll: Boolean, noinline showMessage: (EventMessage) -> Unit, ) { when (this) { @@ -33,11 +34,17 @@ inline fun UnlockWalletError.handle( // This should never happen in this flow, as we always check for the wallet existence before unlocking showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error))) } - is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, analyticsEventHandler, showMessage) + is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock( + isFromUnlockAll = isFromUnlockAll, + error = this, + analyticsEventHandler = analyticsEventHandler, + showDialog = showMessage, + ) } } fun handleUnableToUnlock( + isFromUnlockAll: Boolean, error: UnlockWalletError.UnableToUnlock, analyticsEventHandler: AnalyticsEventHandler, showDialog: (DialogMessage) -> Unit, @@ -46,7 +53,7 @@ fun handleUnableToUnlock( is UnlockWalletError.UnableToUnlock.WithReason -> { when (error.reason) { Reason.AllKeysInvalidated -> { - analyticsEventHandler.send(SignIn.ErrorBiometricUpdated()) + analyticsEventHandler.send(SignIn.ErrorBiometricUpdated(isFromUnlockAll)) DialogMessage( title = resourceReference(R.string.biometric_updated_warning_title), message = resourceReference(R.string.biometric_updated_warning_description), diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt index 9b88374046..f62e70074f 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt @@ -21,7 +21,9 @@ sealed class SignIn( class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric") - class ErrorBiometricUpdated : SignIn(event = "Error - Biometric Updated") + data class ErrorBiometricUpdated( + val isFromUnlockAll: Boolean, + ) : SignIn(event = "Error - Biometric Updated") class ButtonWallet( signInType: SignInType, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index f0b9432ebf..badebd605e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -91,7 +91,7 @@ internal class UserWalletListModel @Inject constructor( private fun onAddNewWalletClick() { if (hotWalletFeatureToggles.isHotWalletEnabled) { - analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.Settings)) router.push(AppRoute.CreateWalletSelection) } else { withProgress(isWalletSavingInProgress) { @@ -109,6 +109,7 @@ internal class UserWalletListModel @Inject constructor( Timber.e("Failed to unlock wallet $userWalletId: $error") error.handle( onUserCancelled = {}, + isFromUnlockAll = false, onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, analyticsEventHandler = analyticsEventHandler, showMessage = messageSender::send, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt index 5e26e5570d..3f86e09a9d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt @@ -31,7 +31,6 @@ internal class HotWalletStepperModel @Inject constructor( } fun onSkipClick() { - // TODO send analytics params.callback.onSkipClick() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index aba0e7c275..8d1c6bd283 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -200,6 +200,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( onAlreadyUnlocked = {}, onUserCancelled = {}, analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = true, showMessage = uiMessageSender::send, ) } diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 249109f0e4..b5b89566f2 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -108,9 +108,10 @@ internal class WelcomeModel @Inject constructor( routedOut = true router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle( + .onLeft { error -> + error.handle( specificWalletId = null, + isFromUnlockAll = true, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }, ) setSelectWalletState() @@ -146,8 +147,12 @@ internal class WelcomeModel @Inject constructor( .onRight { router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle(null, onUserCancelled = { /* ignore */ }) + .onLeft { error -> + error.handle( + specificWalletId = null, + isFromUnlockAll = true, + onUserCancelled = { /* ignore */ }, + ) } } }, @@ -202,11 +207,19 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) } .onLeft { error -> - error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + error.handle( + specificWalletId = userWalletId, + isFromUnlockAll = false, + onUserCancelled = { /* ignore*/ }, + ) } } - suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { + suspend fun UnlockWalletError.handle( + specificWalletId: UserWalletId?, + isFromUnlockAll: Boolean, + onUserCancelled: suspend () -> Unit = { }, + ) { handle( onAlreadyUnlocked = { // this should not happen, as we check for locked state before this @@ -215,6 +228,7 @@ internal class WelcomeModel @Inject constructor( }, onUserCancelled = { onUserCancelled() }, analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = isFromUnlockAll, showMessage = uiMessageSender::send, ) } From 533c0368190b9db88aea0f55d6fc3e13a9f5ebde Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:20:51 +0500 Subject: [PATCH 3/4] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 9 +- .../response/TokenMarketListResponse.kt | 1 + .../local/preferences/PreferencesKeys.kt | 4 +- ...g_yield_supply_in_market_notification.webp | Bin 0 -> 18094 bytes .../markets/DefaultMarketsTokenRepository.kt | 27 +++- .../converters/TokenListConfigConverters.kt | 1 + .../converters/TokenMarketListConverter.kt | 2 +- .../data/markets/di/MarketsDataModule.kt | 1 - .../data/promo/DefaultPromoRepository.kt | 8 +- .../com/tangem/domain/markets/TokenMarket.kt | 2 +- .../domain/markets/TokenMarketListConfig.kt | 2 +- .../GetStakingNotificationMaxApyUseCase.kt | 39 ----- .../ShouldShowYieldModeMarketPromoUseCase.kt | 24 +++ .../repositories/MarketsTokenRepository.kt | 5 +- .../tangem/domain/promo/PromoRepository.kt | 4 +- .../converter/MarketsTokenItemConverter.kt | 2 +- .../feed/model/feed/FeedComponentModel.kt | 1 + .../feed/state/FeedMarketsBatchFlowManager.kt | 1 + .../feed/ui/market/state/MarketsListUM.kt | 1 + .../analytics/MarketsListAnalyticsEvent.kt | 8 +- .../tokenlist/impl/model/MarketsListModel.kt | 81 ++++++---- .../impl/model/MarketsNotificationUM.kt | 22 +++ .../converters/MarketsTokenItemConverter.kt | 2 +- .../MarketsListBatchFlowManager.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 29 ++-- .../markets/tokenlist/impl/ui/MarketsList.kt | 65 ++++---- .../YieldSupplyInMarketsPromoNotification.kt | 147 ++++++++++++++++++ .../tokenlist/impl/ui/state/MarketsListUM.kt | 7 +- 28 files changed, 334 insertions(+), 162 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp delete mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.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 d6bfbdb63a..6e154f3fc1 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 @@ -8,7 +8,6 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -129,13 +128,11 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetStakingNotificationMaxApyUseCase( - settingsRepository: SettingsRepository, + fun provideShouldShowYieldModeMarketPromoUseCase( promoRepository: PromoRepository, marketsTokenRepository: MarketsTokenRepository, - ): GetStakingNotificationMaxApyUseCase { - return GetStakingNotificationMaxApyUseCase( - settingsRepository = settingsRepository, + ): ShouldShowYieldModeMarketPromoUseCase { + return ShouldShowYieldModeMarketPromoUseCase( promoRepository = promoRepository, marketsTokenRepository = marketsTokenRepository, ) 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 3c1829900b..31168b8c5b 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 @@ -26,6 +26,7 @@ data class TokenMarketListResponse( @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @Json(name = "staking_opportunities") val stakingOpportunities: List?, + @Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?, ) { @JsonClass(generateAdapter = true) 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 3fafa4462a..b52614222b 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 @@ -77,8 +77,8 @@ 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 MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy { + booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked") } val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") } diff --git a/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp b/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp new file mode 100644 index 0000000000000000000000000000000000000000..6f7bfc38ce451bddf180a6c653af3ed8ddf30629 GIT binary patch literal 18094 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, - private val maxApyStore: RuntimeStateStore, private val networkFactory: NetworkFactory, excludedBlockchains: ExcludedBlockchains, ) : MarketsTokenRepository { @@ -95,8 +94,6 @@ internal class DefaultMarketsTokenRepository( val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res) - maxApyStore.store(tokenMarketListWithMaxApy.maxApy) - return BatchFetchResult.Success( data = tokenMarketListWithMaxApy.tokens, last = last, @@ -294,8 +291,24 @@ internal class DefaultMarketsTokenRepository( } } - override suspend fun getMaxApy(): Flow { - return maxApyStore.get() + override suspend fun showYieldModePromo( + appCurrency: AppCurrency, + interval: TokenMarketListConfig.Interval, + ): Boolean = try { + val hasYieldSupplyTokens = marketsApi.getCoinsList( + currency = appCurrency.code, + interval = interval.toRequestParam(), + order = TokenMarketListConfig.Order.YieldSupply.toRequestParam(), + offset = 0, + limit = 40, + timestamp = null, + search = null, + ).getOrThrow().tokens.isNotEmpty() + + hasYieldSupplyTokens + } catch (error: Exception) { + Timber.e(error) + false } inline fun catchListErrorAndSendEvent(block: () -> T): T { 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 e280e160e1..7dce907fe8 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 @@ -16,6 +16,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) TokenMarketListConfig.Order.TopGainers -> "gainers" TokenMarketListConfig.Order.TopLosers -> "losers" TokenMarketListConfig.Order.Staking -> "staking" + TokenMarketListConfig.Order.YieldSupply -> "yield" } 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 067f886293..ed6cb59dc4 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 @@ -40,7 +40,7 @@ internal object TokenMarketListConverter : Converter { + override fun isMarketsYieldSupplyNotificationHideClicked(): Flow { return appPreferencesStore.get( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, default = false, ) } - override suspend fun setMarketsStakingNotificationHideClicked() { + override suspend fun setMarketsYieldSupplyNotificationHideClicked() { appPreferencesStore.store( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, value = true, ) } 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 36b179897a..3deeee528d 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,7 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, - val stakingRate: BigDecimal?, + val yieldRate: BigDecimal?, val updateTimestamp: Long?, 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 011df2c630..ffd82b8909 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, Staking + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking, YieldSupply, } enum class Interval { 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 deleted file mode 100644 index 4ccaea6680..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.markets - -import android.icu.util.Calendar -import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.settings.repositories.SettingsRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import java.math.BigDecimal - -class GetStakingNotificationMaxApyUseCase( - private val settingsRepository: SettingsRepository, - private val promoRepository: PromoRepository, - private val marketsTokenRepository: MarketsTokenRepository, -) { - - suspend operator fun invoke(): Flow { - 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/ShouldShowYieldModeMarketPromoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt new file mode 100644 index 0000000000..599e7cefc3 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.promo.PromoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ShouldShowYieldModeMarketPromoUseCase( + private val promoRepository: PromoRepository, + private val marketsTokenRepository: MarketsTokenRepository, +) { + + operator fun invoke(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Flow { + val hideClickedFlow = promoRepository.isMarketsYieldSupplyNotificationHideClicked() + + return hideClickedFlow.map { hideClicked -> + !hideClicked && marketsTokenRepository.showYieldModePromo( + appCurrency = appCurrency, + interval = interval, + ) + } + } +} \ 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 d813fdcc17..00a0e86540 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 @@ -1,11 +1,10 @@ package com.tangem.domain.markets.repositories +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal interface MarketsTokenRepository { @@ -56,5 +55,5 @@ interface MarketsTokenRepository { */ suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List - suspend fun getMaxApy(): Flow + suspend fun showYieldModePromo(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Boolean } \ 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 6e7afbdb02..de65de82b0 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 @@ -16,9 +16,9 @@ interface PromoRepository { suspend fun setNeverToShowTokenPromo(promoId: PromoId) - fun isMarketsStakingNotificationHideClicked(): Flow + fun isMarketsYieldSupplyNotificationHideClicked(): Flow - suspend fun setMarketsStakingNotificationHideClicked() + suspend fun setMarketsYieldSupplyNotificationHideClicked() suspend fun isMoonpayPromoActive(): Boolean // endregion diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt index b513310fb1..6bc58aab6e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter( trendType = value.getTrendType(), chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, - stakingRate = value.stakingRate?.format { percent() }?.let { + stakingRate = value.yieldRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index ee02fa8424..6b7fb68da4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -338,6 +338,7 @@ internal class FeedComponentModel @Inject constructor( SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt index 7fa17ca9f5..b053465040 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt @@ -386,6 +386,7 @@ internal class FeedMarketsBatchFlowManager( TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking + TokenMarketListConfig.Order.YieldSupply -> SortByTypeUM.YieldSupply } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt index 27e1c0daaf..88dc1834e7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt @@ -41,6 +41,7 @@ enum class SortByTypeUM(val text: TextReference) { 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)), + YieldSupply(resourceReference(R.string.markets_sort_by_yield_mode_title)), } @Immutable 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 c5f92d0700..996e95d31b 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 @@ -24,6 +24,7 @@ internal sealed class MarketsListAnalyticsEvent( SortByTypeUM.TopGainers -> "Gainers" SortByTypeUM.TopLosers -> "Losers" SortByTypeUM.Staking -> "Staking" + SortByTypeUM.YieldSupply -> "Yield Supply" }, "Period" to when (interval) { MarketsListUM.TrendInterval.H24 -> "24h" @@ -32,12 +33,11 @@ internal sealed class MarketsListAnalyticsEvent( }, ), ) + class YieldModePromoShown : MarketsListAnalyticsEvent(event = "Notice - Yield Mode Promo") - class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + class YieldModePromoClosed : MarketsListAnalyticsEvent(event = "Yield Mode Promo Closed") - class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") - - class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + class YieldModeMoreInfoClicked : MarketsListAnalyticsEvent(event = "Yield Mode More Info") data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( event = "Token Searched", 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 600133bc7c..e214f30658 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 @@ -10,19 +10,20 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState 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.ShouldShowYieldModeMarketPromoUseCase import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.models.currency.CryptoCurrency 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.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -31,7 +32,6 @@ 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 @@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + shouldShowYieldModeMarketPromoUseCase: ShouldShowYieldModeMarketPromoUseCase, private val promoRepository: PromoRepository, private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @@ -69,8 +69,6 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, - onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) }, - onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, ) @@ -112,53 +110,62 @@ internal class MarketsListModel @Inject constructor( marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> if (isInSearchMode) { combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow5 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = isSearchNotFoundState, - stakingNotificationMaxApy = stakingMaxApy, + shouldShowYieldModePromo = isYieldModePromo, userCountry = userCountry, ) } } else { combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + flow = mainMarketsListManager.uiItems, + flow2 = mainMarketsListManager.isInInitialLoadingErrorState, + flow3 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow4 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = false, - stakingNotificationMaxApy = stakingNotificationMaxApy, + shouldShowYieldModePromo = shouldShowYieldModePromo, userCountry = userCountry, ) } } }.collect { marketsItemsData -> - val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() - } - - if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && - stakingNotificationMaxApy != null - ) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown()) + val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo + if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown()) } marketsListUMStateManager.onUiItemsChanged( uiItems = marketsItemsData.items, isInErrorState = marketsItemsData.isInErrorState, isSearchNotFound = marketsItemsData.isSearchNotFound, - stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + marketsNotificationUM = if (shouldShowYieldModePromo) { + MarketsNotificationUM.YieldSupplyPromo( + onClick = { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModeMoreInfoClicked()) + marketsListUMStateManager.selectedSortByType = SortByTypeUM.YieldSupply + }, + onCloseClick = { onYieldModeNotificationCloseClick() }, + ) + } else { + null }, ) } @@ -266,6 +273,14 @@ internal class MarketsListModel @Inject constructor( mainMarketsListManager.reload() } + fun TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval { + return when (this) { + TrendInterval.H24 -> TokenMarketListConfig.Interval.H24 + TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK + TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH + } + } + private fun initAnalytics() { containerBottomSheetState.onEach { bottomSheetState -> if (bottomSheetState == BottomSheetState.EXPANDED) { @@ -302,10 +317,10 @@ internal class MarketsListModel @Inject constructor( }.saveIn(updateQuotesJob) } - private fun onStakingNotificationCloseClick() { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed()) + private fun onYieldModeNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoClosed()) modelScope.launch { - promoRepository.setMarketsStakingNotificationHideClicked() + promoRepository.setMarketsYieldSupplyNotificationHideClicked() } } @@ -313,7 +328,7 @@ internal class MarketsListModel @Inject constructor( val items: ImmutableList, val isInErrorState: Boolean, val isSearchNotFound: Boolean, - val stakingNotificationMaxApy: BigDecimal?, + val shouldShowYieldModePromo: Boolean, 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/MarketsNotificationUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt new file mode 100644 index 0000000000..5870289d78 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.tokenlist.impl.model + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +internal sealed class MarketsNotificationUM(val config: NotificationConfig) { + + data class YieldSupplyPromo( + val onClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : MarketsNotificationUM( + config = NotificationConfig( + iconResId = R.drawable.img_yield_supply_in_market_notification, + title = resourceReference(R.string.markets_yield_supply_banner_title), + subtitle = TextReference.EMPTY, + onClick = onClick, + onCloseClick = onCloseClick, + ), + ) +} \ 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 ec31aac834..2e0926eaf5 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 @@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter( trendType = value.getTrendType(), chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, - stakingRate = value.stakingRate?.format { percent() }?.let { + stakingRate = value.yieldRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, 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 03b8b24eeb..4847ff333a 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 @@ -343,6 +343,7 @@ internal class MarketsListBatchFlowManager( SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply } } 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 f2c6ee64d3..33f89173a9 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.state.* import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -16,7 +17,6 @@ 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") @@ -26,8 +26,6 @@ internal class MarketsListUMStateManager( private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, - private val onStakingNotificationClick: () -> Unit, - private val onStakingNotificationCloseClick: () -> Unit, private val onShowTokensUnder100kClicked: () -> Unit, ) { @@ -92,25 +90,25 @@ internal class MarketsListUMStateManager( isInErrorState: Boolean, isSearchNotFound: Boolean, uiItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ) { - state.update { + state.update { currentState -> when { isInErrorState -> { - it.copy( + currentState.copy( list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked), ) } isSearchNotFound -> { - it.copy(list = ListUM.SearchNothingFound) + currentState.copy(list = ListUM.SearchNothingFound) } uiItems.isEmpty() -> { - it.copy(list = ListUM.Loading) + currentState.copy(list = ListUM.Loading) } else -> { - it.updateItems( + currentState.updateItems( newItems = uiItems, - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } } @@ -119,7 +117,7 @@ internal class MarketsListUMStateManager( private fun MarketsListUM.updateItems( newItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ): MarketsListUM { val currentState = this @@ -131,7 +129,7 @@ internal class MarketsListUMStateManager( .copy( showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), ), - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } @@ -225,12 +223,7 @@ internal class MarketsListUMStateManager( onOptionClicked = ::onBottomSheetOptionClicked, ), ), - stakingNotificationMaxApy = null, - onStakingNotificationClick = { - onStakingNotificationClick() - selectedSortByType = SortByTypeUM.Staking - }, - onStakingNotificationCloseClick = onStakingNotificationCloseClick, + marketsNotificationUM = null, ) 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 5b80958c46..0930d3c1e4 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -38,19 +37,17 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.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.* -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 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet -import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification +import com.tangem.features.markets.tokenlist.impl.ui.components.YieldSupplyInMarketsPromoNotification import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM @@ -58,7 +55,6 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal private const val SHOW_MORE_KEY = "privacyPolicy" @@ -180,38 +176,38 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif ) } + val marketsNotification = state.marketsNotificationUM AnimatedVisibility( state.isInSearchMode.not() && - state.stakingNotificationMaxApy != null && - state.selectedSortBy != SortByTypeUM.Staking, + state.selectedSortBy != SortByTypeUM.YieldSupply, ) { val showMore = stringResourceSafe(R.string.common_show_more) - val description = stringResourceSafe( - R.string.markets_staking_banner_description_placeholder, - showMore, - ) - val clickableDescription = buildAnnotatedString { - append(description.substringBefore(showMore)) + when (marketsNotification) { + is MarketsNotificationUM.YieldSupplyPromo -> { + val description = stringResourceSafe( + R.string.markets_yield_supply_banner_description, + showMore, + ) - pushStringAnnotation(SHOW_MORE_KEY, "") - appendColored(showMore, TangemTheme.colors.text.accent) - pop() + val clickableDescription = annotatedReference { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + YieldSupplyInMarketsPromoNotification( + config = marketsNotification.config.copy( + subtitle = clickableDescription, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) + } + else -> { /* no-op */ + } } - - 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), - ) } } } @@ -420,9 +416,10 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), - stakingNotificationMaxApy = BigDecimal(0.12345), - onStakingNotificationClick = {}, - onStakingNotificationCloseClick = {}, + marketsNotificationUM = MarketsNotificationUM.YieldSupplyPromo( + onClick = {}, + onCloseClick = {}, + ), ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt new file mode 100644 index 0000000000..e2f16f2735 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.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(0x2684B0D7) +private val borderColor = Color(0x2684B0D7) + +@Composable +fun YieldSupplyInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + var textHeightDp by remember { mutableStateOf(0.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = borderColor, + ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(bgColor) + .clickable { config.onClick?.invoke() }, + ) { + PromoImage( + iconRes = config.iconResId, + modifier = Modifier.height(textHeightDp), + ) + PromoText( + title = config.title, + subtitle = config.subtitle, + onSizeChange = { textHeightDp = it }, + ) + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.secondary, + ) + } +} + +@Composable +private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .requiredWidth(80.dp) + .wrapContentHeight(Alignment.CenterVertically, unbounded = true), + ) + } +} + +@Composable +private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) { + val density = LocalDensity.current + + Box( + modifier = Modifier.onSizeChanged { + with(density) { onSizeChange(it.height.toDp()) } + }, + ) { + TextsBlock( + title = title, + subtitle = subtitle, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.CenterStart) + .padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp), + ) + } +} + +@Composable +private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val titleText = title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + SpacerH(height = TangemTheme.dimens.spacing2) + } + + Text( + text = subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.caption2, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun RingPromoNotification_Preview() { + TangemThemePreview { + YieldSupplyInMarketsPromoNotification( + config = NotificationConfig( + title = stringReference("Activate Yield Mode"), + subtitle = stringReference("Power up your assets while supplying them with instant access. Show more"), + iconResId = R.drawable.img_yield_supply_in_market_notification, + onCloseClick = { }, + ), + ) + } +} + +// endregion \ No newline at end of file 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 da29b428d1..8404856a87 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 @@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import kotlinx.collections.immutable.ImmutableList -import java.math.BigDecimal internal data class MarketsListUM( val list: ListUM, @@ -19,9 +19,7 @@ internal data class MarketsListUM( val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, - val stakingNotificationMaxApy: BigDecimal?, - val onStakingNotificationClick: () -> Unit, - val onStakingNotificationCloseClick: () -> Unit, + val marketsNotificationUM: MarketsNotificationUM?, ) { val isInSearchMode get() = searchBar.isActive @@ -40,6 +38,7 @@ enum class SortByTypeUM(val text: TextReference) { 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)), + YieldSupply(resourceReference(R.string.yield_module_earn_sheet_title)), } @Immutable From 752aaeecd7848cda2cd09d444281bf758d92ff6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 14:21:08 +0000 Subject: [PATCH 4/4] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 462e99b59c..e51eb53245 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1329" +tangemBlockchainSdk = "develop-1327" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.32-574" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^