Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 11:15:09 +05:00
parent d82474324b
commit c0fbd241f4
10 changed files with 91 additions and 4 deletions

View file

@ -25,6 +25,7 @@ dependencies {
api(projects.features.wallet.api) api(projects.features.wallet.api)
api(projects.features.account.api) api(projects.features.account.api)
api(projects.features.commonFeatures.api) api(projects.features.commonFeatures.api)
api(projects.features.marketing.api)
implementation(projects.features.promoBanners.api) implementation(projects.features.promoBanners.api)
/* Data */ /* Data */
@ -54,6 +55,7 @@ dependencies {
implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.models)
implementation(projects.domain.transaction) implementation(projects.domain.transaction)
implementation(projects.domain.news) implementation(projects.domain.news)
implementation(projects.domain.marketing.models)
implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply)
implementation(projects.domain.earn) implementation(projects.domain.earn)

View file

@ -20,6 +20,7 @@ import com.tangem.features.feed.components.news.details.DefaultNewsDetailsCompon
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.components.search.DefaultSearchComponent import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import javax.inject.Inject import javax.inject.Inject
@ -33,6 +34,7 @@ internal class FeedEntryChildFactory @Inject constructor(
private val manageFundsComponentFactory: ManageFundsComponent.Factory, private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory, private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val designFeatureToggles: DesignFeatureToggles, private val designFeatureToggles: DesignFeatureToggles,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) { ) {
@Serializable @Serializable
@ -86,6 +88,7 @@ internal class FeedEntryChildFactory @Inject constructor(
designFeatureToggles = designFeatureToggles, designFeatureToggles = designFeatureToggles,
addToPortfolioComponentFactory = addToPortfolioComponentFactory, addToPortfolioComponentFactory = addToPortfolioComponentFactory,
manageFundsComponentFactory = manageFundsComponentFactory, manageFundsComponentFactory = manageFundsComponentFactory,
marketingBannerComponentFactory = marketingBannerComponentFactory,
) )
} }
is Child.TokenList -> { is Child.TokenList -> {

View file

@ -37,6 +37,7 @@ import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnal
import com.tangem.features.feed.model.market.details.state.TokenNetworksState import com.tangem.features.feed.model.market.details.state.TokenNetworksState
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTitle import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTitle
import com.tangem.features.marketing.api.MarketingBannerComponent
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -51,6 +52,7 @@ internal class DefaultMarketsTokenDetailsComponent(
val params: Params, val params: Params,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory, private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
// applying l2 compatibility // applying l2 compatibility
@ -62,6 +64,11 @@ internal class DefaultMarketsTokenDetailsComponent(
private val analyticsParams = params.analyticsParams private val analyticsParams = params.analyticsParams
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
private val marketingBannerComponent = marketingBannerComponentFactory.create(
context = child("marketsTokenDetailsMarketingBanner"),
params = MarketingBannerComponent.Params.Standalone(requestFlow = model.marketingRequest),
)
private val portfolioComponent: MarketsPortfolioComponent? = private val portfolioComponent: MarketsPortfolioComponent? =
if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) { if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) {
portfolioComponentFactory.create( portfolioComponentFactory.create(
@ -214,6 +221,9 @@ internal class DefaultMarketsTokenDetailsComponent(
component.Content(blockModifier) component.Content(blockModifier)
} }
}, },
marketingBanner = { blockModifier ->
marketingBannerComponent.Content(blockModifier)
},
) )
bottomSheet.child?.instance?.BottomSheet() bottomSheet.child?.instance?.BottomSheet()
addFundsBs.child?.instance?.BottomSheet() addFundsBs.child?.instance?.BottomSheet()

View file

@ -40,6 +40,7 @@ import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.markets.* import com.tangem.domain.markets.*
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.news.model.NewsListConfig import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsUseCase import com.tangem.domain.news.usecase.GetNewsUseCase
@ -49,6 +50,7 @@ import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.marketing.api.MarketingBannerRequest
import com.tangem.features.feed.components.market.details.AddFundsSlotRoute import com.tangem.features.feed.components.market.details.AddFundsSlotRoute
import com.tangem.features.feed.components.market.details.AddToPortfolioSlotRoute import com.tangem.features.feed.components.market.details.AddToPortfolioSlotRoute
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
@ -234,6 +236,10 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val isVisibleOnScreen = MutableStateFlow(false) val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading) val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
val marketingRequest: Flow<MarketingBannerRequest?> = flowOf(
MarketingBannerRequest(screen = MarketingScreen.TokenMarkets(coingeckoId = params.token.id.value)),
)
val addToPortfolioSheetNavigation = SlotNavigation<AddToPortfolioSlotRoute>() val addToPortfolioSheetNavigation = SlotNavigation<AddToPortfolioSlotRoute>()
val addFundsSheetNavigation = SlotNavigation<AddFundsSlotRoute>() val addFundsSheetNavigation = SlotNavigation<AddFundsSlotRoute>()

View file

@ -64,6 +64,7 @@ internal fun MarketsTokenDetailsContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?, portfolioBlock: @Composable ((Modifier) -> Unit)?,
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?, portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
marketingBanner: @Composable (Modifier) -> Unit,
) { ) {
Content( Content(
contentPadding = contentPadding, contentPadding = contentPadding,
@ -72,6 +73,7 @@ internal fun MarketsTokenDetailsContent(
state = state, state = state,
portfolioBlock = portfolioBlock, portfolioBlock = portfolioBlock,
portfolioFloatingBlock = portfolioFloatingBlock, portfolioFloatingBlock = portfolioFloatingBlock,
marketingBanner = marketingBanner,
) )
when (state.bottomSheetConfig.content) { when (state.bottomSheetConfig.content) {
@ -90,6 +92,7 @@ private fun Content(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?, portfolioBlock: @Composable ((Modifier) -> Unit)?,
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?, portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
marketingBanner: @Composable (Modifier) -> Unit,
) { ) {
val isRedesignEnabled = LocalRedesignEnabled.current val isRedesignEnabled = LocalRedesignEnabled.current
val density = LocalDensity.current val density = LocalDensity.current
@ -148,14 +151,13 @@ private fun Content(
) )
} }
item { SpacerH16() } item { SpacerH16() }
tokenMarketDetailsBody( tokenMarketDetailsBody(
state = state.body, state = state.body,
portfolioBlock = portfolioBlock, portfolioBlock = portfolioBlock,
relatedNews = state.relatedNews, relatedNews = state.relatedNews,
isRedesignEnabled = isRedesignEnabled, isRedesignEnabled = isRedesignEnabled,
marketingBanner = marketingBanner,
) )
item { SpacerH(bottomSpacing) } item { SpacerH(bottomSpacing) }
} }
} }
@ -493,6 +495,7 @@ private fun MarketsTokenDetailsContent_Preview(
backgroundColor = TangemTheme.colors.background.tertiary, backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {}, portfolioBlock = {},
portfolioFloatingBlock = null, portfolioFloatingBlock = null,
marketingBanner = {},
contentPadding = PaddingValues(), contentPadding = PaddingValues(),
) )
} }

View file

@ -28,11 +28,13 @@ internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body, state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable ((Modifier) -> Unit)?, portfolioBlock: @Composable ((Modifier) -> Unit)?,
relatedNews: RelatedNews, relatedNews: RelatedNews,
marketingBanner: @Composable (Modifier) -> Unit,
) { ) {
if (isRedesignEnabled) { if (isRedesignEnabled) {
tokenMarketDetailsBodyV2( tokenMarketDetailsBodyV2(
state = state, state = state,
relatedNews = relatedNews, relatedNews = relatedNews,
marketingBanner = marketingBanner,
) )
} else { } else {
tokenMarketDetailsBodyV1( tokenMarketDetailsBodyV1(
@ -95,13 +97,28 @@ private fun LazyListScope.tokenMarketDetailsBodyV1(
} }
} }
private fun LazyListScope.marketingBannerItem(marketingBanner: @Composable (Modifier) -> Unit) {
item(key = "marketing_banner") {
marketingBanner(
Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
)
}
}
// Empty item with a key so that deeplink scroll-to-section can target it before the real content is composed // Empty item with a key so that deeplink scroll-to-section can target it before the real content is composed
private fun LazyListScope.sectionStub(key: String) { private fun LazyListScope.sectionStub(key: String) {
item(key) { } item(key) { }
} }
@Suppress("CanBeNonNullable") @Suppress("CanBeNonNullable")
private fun LazyListScope.tokenMarketDetailsBodyV2(state: MarketsTokenDetailsUM.Body, relatedNews: RelatedNews) { private fun LazyListScope.tokenMarketDetailsBodyV2(
state: MarketsTokenDetailsUM.Body,
relatedNews: RelatedNews,
marketingBanner: @Composable (Modifier) -> Unit,
) {
when (state) { when (state) {
MarketsTokenDetailsUM.Body.Loading -> { MarketsTokenDetailsUM.Body.Loading -> {
item("description-loading") { item("description-loading") {
@ -115,6 +132,8 @@ private fun LazyListScope.tokenMarketDetailsBodyV2(state: MarketsTokenDetailsUM.
description(state.description) description(state.description)
} }
marketingBannerItem(marketingBanner)
infoBlocksListV2( infoBlocksListV2(
state = state.infoBlocks, state = state.infoBlocks,
relatedNews = relatedNews, relatedNews = relatedNews,
@ -169,7 +188,7 @@ private fun LazyListScope.description(description: MarketsTokenDetailsUM.Descrip
modifier = { modifier = {
this this
.padding(horizontal = TangemTheme.dimens2.x4) .padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x8) .padding(bottom = 24.dp)
}, },
otherModifier = { otherModifier = {
blockPaddings() blockPaddings()

View file

@ -14,6 +14,7 @@ dependencies {
/** Feature */ /** Feature */
implementation(projects.features.yieldSupply.api) implementation(projects.features.yieldSupply.api)
implementation(projects.features.marketing.api)
/** Core */ /** Core */
implementation(projects.core.configToggles) implementation(projects.core.configToggles)
@ -59,6 +60,7 @@ dependencies {
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)
implementation(projects.domain.marketing.models)
implementation(projects.libs.crypto) implementation(projects.libs.crypto)
/** Compose */ /** Compose */

View file

@ -21,6 +21,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.active.model.YieldSupplyActiveModel import com.tangem.features.yield.supply.impl.active.model.YieldSupplyActiveModel
@ -38,6 +39,7 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext, @Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyActiveComponent.Params, @Assisted private val params: YieldSupplyActiveComponent.Params,
private val appRouter: AppRouter, private val appRouter: AppRouter,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : YieldSupplyActiveComponent, AppComponentContext by appComponentContext { ) : YieldSupplyActiveComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyActiveModel = getOrCreateModel(params = params) private val model: YieldSupplyActiveModel = getOrCreateModel(params = params)
@ -49,6 +51,14 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
), ),
) )
private val marketingBannerComponent = marketingBannerComponentFactory.create(
context = child("marketingBanner"),
params = MarketingBannerComponent.Params.Standalone(
requestFlow = model.marketingRequest,
onDeeplinkClick = model::onMarketingBannerDeeplink,
),
)
private val bottomSheetSlot = childSlot( private val bottomSheetSlot = childSlot(
key = "yieldSupplyActiveStack", key = "yieldSupplyActiveStack",
source = model.slotNavigation, source = model.slotNavigation,
@ -83,6 +93,7 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
state = state, state = state,
isBalanceHidden = isBalanceHidden, isBalanceHidden = isBalanceHidden,
chartComponent = chartComponent, chartComponent = chartComponent,
marketingBannerComponent = marketingBannerComponent,
onReadMoreClick = model::onReadMoreClick, onReadMoreClick = model::onReadMoreClick,
) )

View file

@ -28,10 +28,15 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.deeplink.resolveMarketingDeeplink
import com.tangem.common.routing.deeplink.toContextualRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.stories.models.StoryContentIds import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.yield.supply.models.YieldBoostStatus import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.usecase.GetYieldBoostStatusUseCase import com.tangem.domain.yield.supply.promo.usecase.GetYieldBoostStatusUseCase
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.marketing.api.MarketingBannerRequest
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
@ -98,6 +103,15 @@ internal class YieldSupplyActiveModel @Inject constructor(
private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency
private var appCurrency = AppCurrency.Default private var appCurrency = AppCurrency.Default
val marketingRequest: Flow<MarketingBannerRequest?> = flowOf(
MarketingBannerRequest(
screen = MarketingScreen.Yield(
networkId = params.cryptoCurrency.network.rawId,
contractAddress = (params.cryptoCurrency as? CryptoCurrency.Token)?.contractAddress.orEmpty(),
),
),
)
val uiState: StateFlow<YieldSupplyActiveContentUM> val uiState: StateFlow<YieldSupplyActiveContentUM>
field = MutableStateFlow( field = MutableStateFlow(
YieldSupplyActiveContentUM( YieldSupplyActiveContentUM(
@ -147,6 +161,16 @@ internal class YieldSupplyActiveModel @Inject constructor(
} }
} }
fun onMarketingBannerDeeplink(deeplink: String): Boolean {
val route = resolveMarketingDeeplink(deeplink).toContextualRoute(
userWalletId = userWalletId,
currency = cryptoCurrency,
screenSource = AnalyticsParam.ScreensSources.Token,
) ?: return false
appRouter.push(route)
return true
}
override fun onDismissClick() { override fun onDismissClick() {
if (!transactionInProgressFlow.value) { if (!transactionInProgressFlow.value) {
slotNavigation.dismiss() slotNavigation.dismiss()

View file

@ -40,6 +40,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -51,6 +52,7 @@ internal fun YieldSupplyActiveContent(
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
onReadMoreClick: () -> Unit, onReadMoreClick: () -> Unit,
chartComponent: ComposableContentComponent, chartComponent: ComposableContentComponent,
marketingBannerComponent: ComposableContentComponent,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -88,6 +90,10 @@ internal fun YieldSupplyActiveContent(
} }
} }
TangemThemeRedesign {
marketingBannerComponent.Content(Modifier.fillMaxWidth())
}
AnimatedVisibility(state.notifications.isNotEmpty()) { AnimatedVisibility(state.notifications.isNotEmpty()) {
val notifications = remember(state.notifications) { state.notifications } val notifications = remember(state.notifications) { state.notifications }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
@ -418,6 +424,7 @@ private fun YieldSupplyActiveBottomSheet_Preview(
state = params, state = params,
isBalanceHidden = true, isBalanceHidden = true,
chartComponent = ComposableContentComponent.EMPTY, chartComponent = ComposableContentComponent.EMPTY,
marketingBannerComponent = ComposableContentComponent.EMPTY,
onReadMoreClick = {}, onReadMoreClick = {},
) )
} }