From a13a54accda0cff8cd488d2ecc54e583e8620244 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Dec 2025 13:24:16 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 20 ++ .../ui/tokens/TokenItemStateConverter.kt | 50 ++++- .../local/preferences/PreferencesKeys.kt | 4 + core/res/src/main/res/values-ja/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 6 +- .../core/ui/components/token/TokenItem.kt | 54 ++++- .../token/internal/YieldSupplyPromoBanner.kt | 110 +++++++++ .../components/token/state/TokenItemState.kt | 19 ++ .../core/ui/test/TokenElementsTestTags.kt | 1 + .../main/res/drawable/ic_rectangle_bottom.xml | 9 + .../tangem/utils/coroutines/CoroutineExt.kt | 8 +- data/yield-supply/build.gradle.kts | 4 + .../supply/DefaultYieldSupplyRepository.kt | 21 +- .../yield/supply/di/YieldSupplyDataModule.kt | 3 + .../yield/supply/YieldSupplyRepository.kt | 4 + .../usecase/YieldSupplyApyFlowUseCase.kt | 5 +- ...ieldSupplyGetShouldShowMainPromoUseCase.kt | 13 ++ ...ieldSupplySetShouldShowMainPromoUseCase.kt | 12 + .../intents/WalletContentClickIntents.kt | 10 + .../implementors/MultiWalletContentLoader.kt | 3 + .../MultiWalletContentLoaderFactory.kt | 3 + .../SingleWalletWithTokenContentLoader.kt | 3 + ...ngleWalletWithTokenContentLoaderFactory.kt | 3 + .../transformers/SetTokenListTransformer.kt | 5 +- .../converter/TokenListStateConverter.kt | 12 +- .../YieldSupplyPromoBannerKeyConverter.kt | 48 ++++ .../subscribers/AccountListSubscriber.kt | 14 +- .../subscribers/BasicAccountListSubscriber.kt | 21 +- .../subscribers/BasicTokenListSubscriber.kt | 16 +- .../MultiWalletTokenListSubscriber.kt | 3 + .../SingleWalletWithTokenListSubscriber.kt | 3 + .../YieldSupplyPromoBannerKeyConverterTest.kt | 208 ++++++++++++++++++ 32 files changed, 665 insertions(+), 35 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt create mode 100644 core/ui/src/main/res/drawable/ic_rectangle_bottom.xml create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt create mode 100644 features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index a151bfa469..28f00e7e98 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -199,4 +199,24 @@ internal object YieldSupplyDomainModule { yieldSupplyRepository = yieldSupplyRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyGetShouldShowMainPromoUseCase { + return YieldSupplyGetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplySetShouldShowMainPromoUseCase { + return YieldSupplySetShouldShowMainPromoUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 9817a2f88a..516a15539c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -43,12 +43,14 @@ import java.math.BigDecimal */ class TokenItemStateConverter( private val appCurrency: AppCurrency, - private val yieldModuleApyMap: Map = emptyMap(), + private val yieldModuleApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val yieldSupplyPromoBannerKey: String? = null, private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) }, private val onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)? = null, + private val onYieldPromoCloseClick: (() -> Unit)? = null, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus -> createTitleState( currencyStatus = currencyStatus, @@ -66,6 +68,15 @@ class TokenItemStateConverter( private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { createFiatAmountState(status = it, appCurrency = appCurrency) }, + private val promoBannerProvider: (CryptoCurrencyStatus) -> TokenItemState.PromoBannerState = { status -> + createPromoBannerState( + status = status, + yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey, + onApyLabelClick = onApyLabelClick, + onYieldPromoCloseClick = onYieldPromoCloseClick, + ) + }, private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, ) : Converter { @@ -102,6 +113,7 @@ class TokenItemStateConverter( subtitleState = requireNotNull(subtitleStateProvider(this)), fiatAmountState = requireNotNull(fiatAmountStateProvider(this)), subtitle2State = requireNotNull(subtitle2StateProvider(this)), + promoBannerState = promoBannerProvider(this), onItemClick = onItemClick?.let { onItemClick -> { onItemClick(it, this) } }, @@ -164,7 +176,7 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?, ): TokenItemState.TitleState { @@ -204,7 +216,7 @@ class TokenItemStateConverter( // polygon-pos_0xc2132d05d31c914a87c6611c10748aeb04b58e8f private fun resolveEarnApy( cryptoCurrencyStatus: CryptoCurrencyStatus, - yieldModuleApyMap: Map, + yieldModuleApyMap: Map, stakingApyMap: Map>, ): EarnApyInfo? { val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token @@ -223,7 +235,7 @@ class TokenItemStateConverter( wrappedList(yieldSupplyApy), ), isActive = isActive, - apy = yieldSupplyApy, + apy = yieldSupplyApy.toString(), ) } } @@ -379,6 +391,36 @@ class TokenItemStateConverter( } } + private fun createPromoBannerState( + status: CryptoCurrencyStatus, + yieldModuleApyMap: Map, + yieldSupplyPromoBannerKey: String?, + onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?, + onYieldPromoCloseClick: (() -> Unit)?, + ): TokenItemState.PromoBannerState { + val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty + if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() || + yieldModuleApyMap[token.yieldSupplyKey()] == null + ) { + return TokenItemState.PromoBannerState.Empty + } + val yieldSupplyApy = + yieldModuleApyMap[token.yieldSupplyKey()] ?: return TokenItemState.PromoBannerState.Empty + + return TokenItemState.PromoBannerState.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(yieldSupplyApy), + ), + onPromoBannerClick = { + onApyLabelClick?.invoke(status, yieldSupplyApy.toString()) + }, + onCloseClick = { + onYieldPromoCloseClick?.invoke() + }, + ) + } + private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange 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 2c128557a6..f3f7fbf07d 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 @@ -122,6 +122,10 @@ object PreferencesKeys { val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") } + val YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY by lazy { + booleanPreferencesKey(name = "yieldSupplyShouldShowMainPromo") + } + val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") } // region Notifications diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3979991fb5..849f728af4 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -590,6 +590,7 @@ まずバックアップを完了する まずバックアップを完了する その他の方法 + リカバリーフレーズは、ご自身で安全な場所に保管し、資金を守るために他人には絶対に共有しないでください。 リカバリーフレーズ アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 ウォレットをハードウェアにアップグレードするには、まずバックアップしてください。 @@ -1432,6 +1433,8 @@ カードの発行に失敗しました 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 技術的なエラーが発生しました。サポートへお問い合わせください。 + 暗号資産を日常の支払いに使おう。\n\nこれまでにないタイプの決済カード。 + Tangem Payを入手 通常は最大で15分ほどかかります Tangemカードのセットアップ カードを発行しています @@ -1454,6 +1457,7 @@ 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在サービスに接続できません。後ほどもう一度お試しください。 + Tangem Visaカード Tangem Payは一時的に利用できません Tangem Pay カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 @@ -1500,6 +1504,7 @@ 複数のアドレス 現在、このブロックチェーンでは取引履歴はサポートされていません。しかしご心配なく!弊社で対応中です。その間、エクスプローラーで確認することができます。 オペレーション + 対象:%s 送金元: %s 送金先: %s バリデーター: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1dd7761587..0bbcf90b6a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -527,7 +527,7 @@ Buying %s Buying %s... Hide this transaction - Once hidden, the transaction status cannot be viewed again. You can simply swipe to dismiss instead. + If you hide this transaction, it won’t appear in the status screen anymore. If you just want to close the status screen and return later, simply swipe it away instead. Hide Transaction Status? This token is not supported. Please choose a different token to swap. %s is not supported @@ -697,11 +697,13 @@ APY %s My portfolio Market + Earn with Tangem To generate addresses for selected networks, you must scan your Tangem Wallet card or ring To add tokens pull this up or tap the search bar This section’s data is sourced from the following networks: %s Unable to load the data… No data + Market Pulse Quick actions Search through the market Result @@ -1985,7 +1987,7 @@ Tangem also takes a 15% service fee on yield generated. Your funds will be automatically supplied to Aave once network fees are lower or your balance meets the minimum required amount. Historical returns - "Enable %1$s%% APY on your balance" + Enable %1$s%% APY on your balance Approval for your token in Yield Mode has been revoked. Open the token to grant permission again. Token approval needed Check your network connection diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 374b6de2e3..fc56519c01 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.currency.icon.CurrencyIcon @@ -28,6 +30,7 @@ import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State +import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -44,7 +47,7 @@ private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3 private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32 private enum class LayoutId { - ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT, PROMO_BANNER } /** @@ -109,6 +112,14 @@ fun TokenItem( .testTag(TokenElementsTestTags.TOKEN_ICON), ) + YieldSupplyPromoBanner( + state = state.promoBannerState, + modifier = Modifier + .layoutId(layoutId = LayoutId.PROMO_BANNER) + .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .fillMaxWidth(), + ) + TokenTitle( state = state.titleState, modifier = Modifier @@ -220,6 +231,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints) + val promoBanner = when (state.promoBannerState) { + is PromoBannerState.Content -> measurables.measure( + layoutId = LayoutId.PROMO_BANNER, + constraints = constraints, + ) + else -> null + } var firstRowRemainingFreeSpace: Int? = null var secondRowRemainingFreeSpace: Int? = null @@ -283,10 +301,18 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) } + val promoBannerHeight = promoBanner?.height ?: 0 + val promoOffset = if (promoBannerHeight > 0) { + promoBannerHeight - 8.dp.roundToPx() + } else { + 0 + } + val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, layoutPadding = verticalPadding, + promoOffset = promoOffset, title = title, fiatAmount = fiatAmount, cryptoAmount = cryptoAmount, @@ -294,16 +320,22 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2)) + promoBanner?.placeRelative(x = 0, y = 0) + + icon.placeRelative( + x = 0, + y = promoOffset + (layoutHeight - promoOffset - icon.height) + .div(other = 2), + ) title.placeRelative( x = icon.width, - y = when (state) { + y = promoOffset + when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { if (state.subtitleState == null) { - (layoutHeight - title.height).div(other = 2) + (layoutHeight - promoOffset - title.height).div(other = 2) } else { verticalPadding } @@ -314,8 +346,8 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier fiatAmount?.placeRelative( x = layoutWidth - fiatAmount.width, - y = when (state.subtitle2State) { - null -> (layoutHeight - fiatAmount.height).div(other = 2) + y = promoOffset + when (state.subtitle2State) { + null -> (layoutHeight - promoOffset - fiatAmount.height).div(other = 2) else -> verticalPadding }, ) @@ -335,7 +367,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier nonFiatContent.placeRelative( x = layoutWidth - nonFiatContent.width, - y = (layoutHeight - nonFiatContent.height).div(other = 2), + y = promoOffset + (layoutHeight - promoOffset - nonFiatContent.height).div(other = 2), ) } } @@ -443,6 +475,7 @@ private fun calculateLayoutHeight( state: TokenItemState, minLayoutHeight: Int, layoutPadding: Int, + promoOffset: Int, title: Placeable, fiatAmount: Placeable?, cryptoAmount: Placeable?, @@ -468,7 +501,7 @@ private fun calculateLayoutHeight( } } - return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight) + return (promoOffset + max(firstColumnHeight, secondColumnHeight)).coerceAtLeast(promoOffset + minLayoutHeight) } @Preview(widthDp = 360, showBackground = true) @@ -587,6 +620,11 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider YieldSupplyPromoBanner(state = state, modifier = modifier) + is PromoBannerState.Empty -> Unit + } +} + +@Composable +internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: Modifier = Modifier) { + val bgColor = TangemTheme.colors.control.unchecked + Column(modifier = modifier) { + Row( + modifier = Modifier + .background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 8.dp) + .clickable(onClick = state.onPromoBannerClick) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size16), + ) + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .weight(1f) + .padding(end = TangemTheme.dimens.spacing8), + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.secondary, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = { state.onCloseClick() }, + ), + ) + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rectangle_bottom), + contentDescription = null, + tint = bgColor, + modifier = Modifier + .size(width = 12.dp, height = 8.dp), + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_YieldSupplyPromoBanner() { + TangemThemePreview { + YieldSupplyPromoBanner( + state = PromoBannerState.Content( + title = TextReference.Str(value = "Earn up to 5% APY"), + onPromoBannerClick = {}, + onCloseClick = {}, + ), + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 7f05cdb7cb..a2180bb614 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -34,6 +34,8 @@ sealed class TokenItemState { */ abstract val subtitle2State: Subtitle2State? + abstract val promoBannerState: PromoBannerState + /** Callback which will be called when an item is clicked */ abstract val onItemClick: ((TokenItemState) -> Unit)? @@ -59,6 +61,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val subtitle2State: Subtitle2State = Subtitle2State.Loading + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -75,6 +78,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val subtitle2State: Subtitle2State = Subtitle2State.Locked + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -99,6 +103,7 @@ sealed class TokenItemState { override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState?, override val subtitle2State: Subtitle2State?, + override val promoBannerState: PromoBannerState = PromoBannerState.Empty, override val onItemClick: ((TokenItemState) -> Unit)?, override val onItemLongClick: ((TokenItemState) -> Unit)?, override val onApyLabelClick: ((TokenItemState) -> Unit)? = null, @@ -120,6 +125,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null @@ -146,6 +152,7 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val subtitle2State: Subtitle2State? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } /** @@ -168,6 +175,7 @@ sealed class TokenItemState { override val subtitle2State: Subtitle2State? = null override val onItemClick: ((TokenItemState) -> Unit)? = null override val onApyLabelClick: ((TokenItemState) -> Unit)? = null + override val promoBannerState: PromoBannerState = PromoBannerState.Empty } @Immutable @@ -254,4 +262,15 @@ sealed class TokenItemState { data object Locked : Subtitle2State() } + + @Immutable + sealed class PromoBannerState { + data class Content( + val title: TextReference, + val onPromoBannerClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : PromoBannerState() + + data object Empty : PromoBannerState() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt index a6c08721f7..d513f7791c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt @@ -7,4 +7,5 @@ object TokenElementsTestTags { const val TOKEN_FIAT_AMOUNT = "TOKEN_FIAT_AMOUNT" const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT" const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK" + const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml new file mode 100644 index 0000000000..6b77c79cb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rectangle_bottom.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 8a2fd97b72..7d14d15efb 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -69,15 +69,16 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } @Suppress("LongParameterList", "MagicNumber") -inline fun combine6( +inline fun combine7( flow1: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, flow6: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, -): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + flow7: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6, flow7) { arr -> @Suppress("UNCHECKED_CAST") transform( arr[0] as T1, @@ -86,5 +87,6 @@ inline fun combine6( arr[3] as T4, arr[4] as T5, arr[5] as T6, + arr[6] as T7, ) } \ No newline at end of file diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index 4fbb4f314a..cc7313cd67 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -19,6 +19,10 @@ dependencies { /** Tangem SDKs */ implementation(tangemDeps.blockchain) + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + /** Core */ implementation(projects.core.datasource) implementation(projects.core.utils) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 384cb82f4b..a40499ba87 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -12,6 +12,10 @@ import com.tangem.data.yield.supply.converters.YieldTokenChartConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -35,6 +39,7 @@ internal class DefaultYieldSupplyRepository( private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val appPreferencesStore: AppPreferencesStore, ) : YieldSupplyRepository { private val statusMap: MutableMap = ConcurrentHashMap() @@ -174,10 +179,18 @@ internal class DefaultYieldSupplyRepository( null } - private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { txInfo -> - txInfo.type is TxInfo.TransactionType.YieldSupply.Enter || - txInfo.type == TxInfo.TransactionType.Approve && - (txInfo.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress + override fun getShouldShowYieldPromoBanner(): Flow { + return appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true) + } + + override suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) { + appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, shouldShow) + } + + private fun Set.hasYieldEnterTransactions(yieldAddress: String) = any { + it.type == TxInfo.TransactionType.YieldSupply.Enter || + it.type == TxInfo.TransactionType.Approve && + (it.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress } private fun Set.hasYieldExitTransactions() = any { diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 409d1da57f..ee040a543a 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.datasource.api.tangemTech.YieldSupplyApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyRepository @@ -41,6 +42,7 @@ internal object YieldSupplyDataModule { walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, analyticsExceptionHandler: AnalyticsExceptionHandler, + appPreferencesStore: AppPreferencesStore, ): YieldSupplyRepository { return DefaultYieldSupplyRepository( yieldSupplyApi = yieldSupplyApi, @@ -48,6 +50,7 @@ internal object YieldSupplyDataModule { dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, analyticsExceptionHandler = analyticsExceptionHandler, + appPreferencesStore = appPreferencesStore, ) } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 8ab13bbac1..9a73322ca8 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -108,4 +108,8 @@ interface YieldSupplyRepository { userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): YieldSupplyEnterStatus? + + fun getShouldShowYieldPromoBanner(): Flow + + suspend fun setShouldShowYieldPromoBanner(shouldShow: Boolean) } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index d93fcca1a1..3dda098013 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.yield.supply.usecase import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.math.BigDecimal /** * Emits a map of APY values per token. @@ -15,11 +16,11 @@ class YieldSupplyApyFlowUseCase( private val yieldSupplyRepository: YieldSupplyRepository, ) { - operator fun invoke(): Flow> { + operator fun invoke(): Flow> { return yieldSupplyRepository.getMarketsFlow() .map { yieldMarketTokenList -> yieldMarketTokenList.filter { it.isActive }.associate { token -> - token.yieldSupplyKey to token.apy.toString() + token.yieldSupplyKey to token.apy } } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..42a1493d5b --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetShouldShowMainPromoUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository +import kotlinx.coroutines.flow.Flow + +class YieldSupplyGetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + operator fun invoke(): Flow { + return yieldSupplyRepository.getShouldShowYieldPromoBanner() + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt new file mode 100644 index 0000000000..dcc0a985e6 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplySetShouldShowMainPromoUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplySetShouldShowMainPromoUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(shouldShow: Boolean) { + yieldSupplyRepository.setShouldShowYieldPromoBanner(shouldShow) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 8c1278e5b6..0cd9d5e7b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -21,6 +21,7 @@ import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap @@ -52,6 +53,8 @@ internal interface WalletContentClickIntents { fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, apy: String) + fun onYieldPromoCloseClick() + fun onAccountExpandClick(account: Account) fun onAccountCollapseClick(account: Account) @@ -89,6 +92,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val accountDependencies: AccountDependencies, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, + private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -182,6 +186,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } + override fun onYieldPromoCloseClick() { + modelScope.launch { + yieldSupplySetShouldShowMainPromoUseCase(false) + } + } + override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 0ce2d80a8d..e08e469afc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -45,6 +46,7 @@ internal class MultiWalletContentLoader( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -63,6 +65,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) WalletNFTListSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index d3fe3d60a4..1f9d0570e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -44,6 +45,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, @@ -72,6 +74,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 7f3e9f91b6..6d895697ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -6,6 +6,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -35,6 +36,7 @@ internal class SingleWalletWithTokenContentLoader( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -49,6 +51,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 0f6ac06ef5..4d89203c74 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -7,6 +7,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -36,6 +37,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -55,6 +57,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4d102bb5d8..0dec24b4b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -11,14 +11,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber +import java.math.BigDecimal internal class SetTokenListTransformer( private val params: TokenConverterParams, private val userWallet: UserWallet, private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, - private val yieldSupplyApyMap: Map = emptyMap(), + private val yieldSupplyApyMap: Map = emptyMap(), private val stakingApyMap: Map> = emptyMap(), + private val shouldShowMainPromo: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -63,6 +65,7 @@ internal class SetTokenListTransformer( clickIntents = clickIntents, yieldModuleApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index f9eefeccd6..9037cb7b57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -28,17 +28,25 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig +@Suppress("LongParameterList") internal class TokenListStateConverter( private val appCurrency: AppCurrency, private val params: TokenConverterParams, private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, - private val yieldModuleApyMap: Map, + private val yieldModuleApyMap: Map, private val stakingApyMap: Map>, + private val shouldShowMainPromo: Boolean, ) : Converter { + private val yieldSupplyPromoBannerKeyConverter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) @@ -57,10 +65,12 @@ internal class TokenListStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, + yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params), stakingApyMap = stakingApyMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apy -> onApyLabelClick(status, apy) }, + onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, ) override fun convert(value: WalletTokensListState): WalletTokensListState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt new file mode 100644 index 0000000000..dd2569e500 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class YieldSupplyPromoBannerKeyConverter( + private val yieldModuleApyMap: Map, + private val shouldShowMainPromo: Boolean, +) : Converter { + + override fun convert(value: TokenConverterParams): String? { + if (!shouldShowMainPromo) return null + + val currencies = when (value) { + is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies() + is TokenConverterParams.Account -> value.accountList.flattenCurrencies() + }.filter { status -> + status.value is CryptoCurrencyStatus.Loaded || + status.value is CryptoCurrencyStatus.Custom + } + + val tokens = currencies.filter { it.currency is CryptoCurrency.Token } + + if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + + val max = tokens.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val tokenKey = token.yieldSupplyKey() + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.second + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index 1a01d99ec3..d1cbb38064 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -5,16 +5,18 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.utils.coroutines.combine6 +import com.tangem.utils.coroutines.combine7 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import java.math.BigDecimal /** * Subscriber that monitors account list related data and updates the wallet state accordingly. @@ -30,19 +32,21 @@ internal class AccountListSubscriber @AssistedInject constructor( override val clickIntents: WalletClickIntents, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6( + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), flow6 = stakingApyFlow(), + flow7 = yieldSupplyGetShouldShowMainPromoFlow(), transform = ::updateState, ) - private fun yieldSupplyApyFlow(): Flow> { + private fun yieldSupplyApyFlow(): Flow> { return yieldSupplyApyFlowUseCase().distinctUntilChanged() } @@ -50,6 +54,10 @@ internal class AccountListSubscriber @AssistedInject constructor( return stakingApyFlowUseCase().distinctUntilChanged() } + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow { + return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged() + } + @AssistedFactory interface Factory { fun create(userWallet: UserWallet): AccountListSubscriber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 40a9681b13..080766b9b3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -20,6 +20,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenCon import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import timber.log.Timber +import java.math.BigDecimal /** * Basic implementation of [WalletSubscriber] for wallet with accounts. @@ -46,8 +47,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency: AppCurrency, expandedAccounts: Set, isAccountMode: Boolean, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean = false, ) { val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount @@ -67,6 +69,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { portfolioId = PortfolioId(mainAccount.accountId), yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } isAccountMode -> { @@ -82,7 +85,13 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } else { val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) + updateContent( + params = convertParams, + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, + ) } } } @@ -92,8 +101,9 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { maybeTokenList: Lce, appCurrency: AppCurrency, portfolioId: PortfolioId, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> @@ -123,14 +133,16 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) } private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map = emptyMap(), + yieldSupplyApyMap: Map = emptyMap(), stakingApyMap: Map> = emptyMap(), + shouldShowMainPromo: Boolean, ) { stateController.update( SetTokenListTransformer( @@ -140,6 +152,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index e827068c9b..36de78ef22 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker @@ -28,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal @Deprecated("Use AccountListSubscriber instead") @Suppress("LongParameterList") @@ -40,6 +42,7 @@ internal abstract class BasicTokenListSubscriber( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -69,7 +72,8 @@ internal abstract class BasicTokenListSubscriber( flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), flow4 = stakingApyFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap -> + flow5 = yieldSupplyGetShouldShowMainPromoFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -98,6 +102,7 @@ internal abstract class BasicTokenListSubscriber( appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ) walletWithFundsChecker.check(tokenList) @@ -122,8 +127,9 @@ internal abstract class BasicTokenListSubscriber( private fun updateContent( params: TokenConverterParams, appCurrency: AppCurrency, - yieldSupplyApyMap: Map, + yieldSupplyApyMap: Map, stakingApyMap: Map>, + shouldShowMainPromo: Boolean, ) { stateHolder.update( SetTokenListTransformer( @@ -133,6 +139,7 @@ internal abstract class BasicTokenListSubscriber( clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, ), ) } @@ -146,9 +153,12 @@ internal abstract class BasicTokenListSubscriber( } .distinctUntilChanged() - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() + private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() private fun stakingApyFlow(): Flow>> = stakingApyFlowUseCase() .distinctUntilChanged() + + private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() + .distinctUntilChanged() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 36220cd6e1..d75d637dad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -12,6 +12,7 @@ import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -32,6 +33,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -41,6 +43,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e686dd4e20..9e148dcf24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore @@ -27,6 +28,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, stakingApyFlowUseCase: StakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -36,6 +38,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingApyFlowUseCase = stakingApyFlowUseCase, + yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt new file mode 100644 index 0000000000..458cbcaef2 --- /dev/null +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -0,0 +1,208 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.yield.supply.YieldSupplyStatus +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import org.junit.Test +import java.math.BigDecimal + +class YieldSupplyPromoBannerKeyConverterTest { + + @Test + fun `GIVEN promo disabled WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF") + val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) + val tokenList = ungroupedTokenList(status) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = tokenList, + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")), + shouldShowMainPromo = false, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN empty apy map WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") + val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = emptyMap(), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN active yield token present WHEN convert THEN return null`() { + val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") + val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusActive), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.12")), + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + @Test + fun `GIVEN multiple candidates EVM case insensitive WHEN convert THEN return key of max amount`() { + val evmNetworkId = "ETH" + val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd") + val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF") + + val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false) + val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) + + val apyMap = mapOf( + "${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), + "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"), + ) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(statusSmall, statusBig), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + val expectedKey = "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" + assertThat(result).isEqualTo(expectedKey) + } + + @Test + fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() { + val nonEvmId = "xrp" + val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123") + val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) + + val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}" + val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) + + val params = TokenConverterParams.Wallet( + portfolioId = PortfolioId.Wallet(UserWalletId("00")), + tokenList = ungroupedTokenList(status), + ) + val converter = YieldSupplyPromoBannerKeyConverter( + yieldModuleApyMap = apyMap, + shouldShowMainPromo = true, + ) + + val result = converter.convert(params) + + assertThat(result).isNull() + } + + private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped { + return TokenList.Ungrouped( + totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded( + amount = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), + sortedBy = com.tangem.domain.models.TokensSortType.NONE, + currencies = statuses.toList(), + ) + } + + private fun createStatus( + token: CryptoCurrency.Token, + amount: BigDecimal, + isYieldActive: Boolean, + ): CryptoCurrencyStatus { + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "addr", + type = NetworkAddress.Address.Type.Primary, + ), + ) + val value = CryptoCurrencyStatus.Custom( + amount = amount, + fiatAmount = null, + fiatRate = null, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = if (isYieldActive) { + YieldSupplyStatus( + isActive = true, + isInitialized = true, + isAllowedToSpend = true, + effectiveProtocolBalance = null, + ) + } else { + null + }, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = networkAddress, + sources = CryptoCurrencyStatus.Sources(), + ) + return CryptoCurrencyStatus( + currency = token, + value = value, + ) + } + + private fun createToken(networkId: String, backendId: String, contract: String): CryptoCurrency.Token { + val network = Network( + id = Network.ID(value = networkId, derivationPath = Network.DerivationPath.None), + backendId = backendId, + name = backendId, + currencySymbol = "SYM", + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = when (backendId) { + "ethereum" -> Network.StandardType.ERC20 + else -> Network.StandardType.Unspecified("UNSPEC") + }, + hasFiatFeeRate = false, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(networkId), + suffix = CryptoCurrency.ID.Suffix.ContractAddress(contract), + ), + network = network, + name = "Token", + symbol = "TKN", + decimals = 18, + iconUrl = null, + isCustom = false, + contractAddress = contract, + ) + } +} \ No newline at end of file