From 12d4917dfc42949cb9ecee9801d2eef5f7bd85c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jun 2026 12:44:54 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/common/ui/earn/EarnBlock.kt | 89 +++++++++++++++++++ .../com/tangem/common/ui/earn/EarnBlockUM.kt | 15 ++++ .../impl/main/model/YieldSupplyModel.kt | 3 - .../YieldSupplyToEarnBlockConverter.kt | 22 ++++- .../YieldSupplyToEarnBlockConverterTest.kt | 28 ++++++ 5 files changed, 153 insertions(+), 4 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt index fbd260125a..9f024aa283 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt @@ -21,7 +21,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -57,6 +61,7 @@ fun EarnBlock(state: EarnBlockUM, modifier: Modifier = Modifier) { when (state) { is EarnBlockUM.Loading -> EarnBlockLoading(modifier) is EarnBlockUM.Content -> EarnBlockContent(state, modifier) + is EarnBlockUM.Promo -> EarnBlockPromo(state, modifier) } } @@ -136,6 +141,74 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo ) } +@Composable +private fun EarnBlockPromo(state: EarnBlockUM.Promo, modifier: Modifier = Modifier) { + val shape = RoundedCornerShape(TangemTheme.dimens2.x5) + Column( + modifier = modifier + .clip(shape) + .backgroundModifier(state.type, state.backgroundUM, shape) + .padding(all = TangemTheme.dimens2.x4), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + EarnBlockIcon( + type = state.type, + iconUM = state.iconUM, + modifier = Modifier.padding(end = TangemTheme.dimens2.x3), + ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + modifier = Modifier.weight(1f), + ) { + Text( + text = state.title.resolveAnnotatedReference(), + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography2.captionMedium12, + color = state.type.accentText(), + ) + } + } + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2)) { + EarnBlockPromoButton( + text = resourceReference(CoreResR.string.common_learn_more), + type = TangemButtonType.Secondary, + onClick = state.onSecondaryClick, + modifier = Modifier.weight(1f), + ) + EarnBlockPromoButton( + text = resourceReference(CoreResR.string.common_activate), + type = EarnBlockUM.TrailingUM.Button.Style.Default.buttonType(state.type), + onClick = state.onPrimaryClick, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun EarnBlockPromoButton( + text: TextReference, + type: TangemButtonType, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TangemButton( + buttonUM = TangemButtonUM( + text = text, + type = type, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + onClick = onClick, + ), + modifier = modifier, + ) +} + @Composable private fun Modifier.backgroundModifier( type: Type, @@ -459,6 +532,22 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterProvider( collection = listOf( + // Promo — boosted APY offer: AccentSoft background, two buttons below + EarnBlockUM.Promo( + type = Type.YieldSupply, + backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft, + iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40), + title = annotatedReference( + buildAnnotatedString { + append("Special offer for Yield mode\nAPY ") + withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) { append("5.1%") } + append(" x3 → 15.3%") + }, + ), + subtitle = stringReference("First time activation bonus!"), + onPrimaryClick = {}, + onSecondaryClick = {}, + ), // Available — promo entry: AccentSoft background, "More" button EarnBlockUM.Content( type = Type.YieldSupply, diff --git a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlockUM.kt b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlockUM.kt index c386bd3244..3739f2591b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlockUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlockUM.kt @@ -19,6 +19,21 @@ sealed interface EarnBlockUM { val onClick: (() -> Unit)? = null, ) : EarnBlockUM + /** + * Promo variant — a column with the [iconUM] + annotated multiline [title] + [subtitle] row on top + * and a pair of full-width buttons below. Button labels are fixed and hardcoded in the composable, + * so only their click handlers ([onSecondaryClick], [onPrimaryClick]) are exposed here. + */ + data class Promo( + val type: Type, + val backgroundUM: BackgroundUM, + val iconUM: IconUM, + val title: TextReference, + val subtitle: TextReference, + val onPrimaryClick: () -> Unit, + val onSecondaryClick: () -> Unit, + ) : EarnBlockUM + enum class Type { Staking, YieldSupply } @Immutable diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 3a6df47bf7..ede3b3e82a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -8,7 +8,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference @@ -70,7 +69,6 @@ internal class YieldSupplyModel @Inject constructor( private val isYieldBoostPromoEnabledForTokenUseCase: IsYieldBoostPromoEnabledForTokenUseCase, private val getBoostedApyUseCase: GetBoostedApyUseCase, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, - private val designFeatureToggles: DesignFeatureToggles, private val boostStoryPreloader: YieldBoostStoryPreloader, ) : Model(), YieldSupplyClickIntents { @@ -163,7 +161,6 @@ internal class YieldSupplyModel @Inject constructor( yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) .onRight { tokenStatus -> val isPromoEnabled = yieldSupplyFeatureToggles.isYieldPromoEnabled && - !designFeatureToggles.isRedesignEnabled && isYieldBoostPromoEnabledForTokenUseCase(params.userWalletId, cryptoCurrencyToken) .getOrElse { false } val boostedApy = if (isPromoEnabled) getBoostedApyUseCase(tokenStatus.apy) else null diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverter.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverter.kt index 2dbde67434..2ef4838e30 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverter.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverter.kt @@ -1,7 +1,9 @@ package com.tangem.features.yield.supply.impl.main.model.converter import com.tangem.common.ui.earn.EarnBlockUM +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.utils.converter.Converter @@ -21,7 +23,25 @@ internal class YieldSupplyToEarnBlockConverter : Converter EarnBlockUM.Loading } - private fun buildAvailable(value: YieldSupplyUM.Available): EarnBlockUM.Content { + private fun buildAvailable(value: YieldSupplyUM.Available): EarnBlockUM = if (value.isBoostAvailable) { + buildBoostedPromo(value) + } else { + buildAvailableContent(value) + } + + private fun buildBoostedPromo(value: YieldSupplyUM.Available): EarnBlockUM.Promo { + return EarnBlockUM.Promo( + type = EarnBlockUM.Type.YieldSupply, + backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft, + iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_yield_40), + title = combinedReference(value.title, stringReference("\n"), value.apyText), + subtitle = resourceReference(CoreResR.string.yield_apy_boost_banner_subtitle), + onPrimaryClick = value.onClick, + onSecondaryClick = value.onLearnMoreClick, + ) + } + + private fun buildAvailableContent(value: YieldSupplyUM.Available): EarnBlockUM.Content { return EarnBlockUM.Content( type = EarnBlockUM.Type.YieldSupply, backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft, diff --git a/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverterTest.kt b/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverterTest.kt index b1279d5ce8..fb624df715 100644 --- a/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverterTest.kt +++ b/features/yield-supply/impl/src/test/java/com/tangem/features/yield/supply/impl/main/model/converter/YieldSupplyToEarnBlockConverterTest.kt @@ -183,4 +183,32 @@ internal class YieldSupplyToEarnBlockConverterTest { content.onClick?.invoke() assertThat(clicked).isTrue() } + + @Test + fun `GIVEN Available isBoostAvailable WHEN convert THEN Promo with both button callbacks`() { + var activateClicked = false + var learnMoreClicked = false + val available = YieldSupplyUM.Available( + apy = "5.1", + apyText = stringReference("APY 5.1% x3 → 15.3%"), + title = stringReference("Special offer for Yield mode"), + onClick = { activateClicked = true }, + onLearnMoreClick = { learnMoreClicked = true }, + isBoostAvailable = true, + ) + + val result = converter.convert(available) + + assertThat(result).isInstanceOf(EarnBlockUM.Promo::class.java) + val promo = result as EarnBlockUM.Promo + assertThat(promo.type).isEqualTo(EarnBlockUM.Type.YieldSupply) + assertThat(promo.backgroundUM).isEqualTo(EarnBlockUM.BackgroundUM.AccentSoft) + assertThat(promo.iconUM).isInstanceOf(EarnBlockUM.IconUM.Glowing::class.java) + + promo.onSecondaryClick() + assertThat(learnMoreClicked).isTrue() + + promo.onPrimaryClick() + assertThat(activateClicked).isTrue() + } } \ No newline at end of file