Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 08:15:09 +02:00
parent aba2f43c34
commit de3e88227a
10 changed files with 91 additions and 4 deletions

View file

@ -23,6 +23,7 @@ dependencies {
api(projects.features.forYou.api)
api(projects.features.promoBanners.api)
api(projects.features.tokenRecieve.api)
api(projects.features.marketing.api)
/* Data */
implementation(projects.data.common)
@ -49,6 +50,7 @@ dependencies {
implementation(projects.domain.core)
implementation(projects.domain.feedback.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.marketing.models)
implementation(projects.domain.yieldSupply.models)
runtimeOnly(projects.domain.manageTokens)
runtimeOnly(projects.domain.offramp)

View file

@ -24,6 +24,7 @@ import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.foryou.ForYouComponent
import com.tangem.features.foryou.TokenSummaryComponent
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@ -39,6 +40,7 @@ internal class FeedEntryChildFactory @Inject constructor(
private val designFeatureToggles: DesignFeatureToggles,
private val forYouComponentFactory: ForYouComponent.Factory,
private val tokenSummaryComponentFactory: TokenSummaryComponent.Factory,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) {
@Serializable
@ -103,6 +105,7 @@ internal class FeedEntryChildFactory @Inject constructor(
designFeatureToggles = designFeatureToggles,
addToPortfolioComponentFactory = addToPortfolioComponentFactory,
manageFundsComponentFactory = manageFundsComponentFactory,
marketingBannerComponentFactory = marketingBannerComponentFactory,
)
}
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.ui.market.detailed.MarketsTokenDetailsContent
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTitle
import com.tangem.features.marketing.api.MarketingBannerComponent
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@ -51,6 +52,7 @@ internal class DefaultMarketsTokenDetailsComponent(
val params: Params,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
// applying l2 compatibility
@ -62,6 +64,11 @@ internal class DefaultMarketsTokenDetailsComponent(
private val analyticsParams = params.analyticsParams
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? =
if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) {
portfolioComponentFactory.create(
@ -214,6 +221,9 @@ internal class DefaultMarketsTokenDetailsComponent(
component.Content(blockModifier)
}
},
marketingBanner = { blockModifier ->
marketingBannerComponent.Content(blockModifier)
},
)
bottomSheet.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.models.FeedbackEmailType
import com.tangem.domain.markets.*
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.news.model.NewsListConfig
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.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
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.AddToPortfolioSlotRoute
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
@ -234,6 +236,10 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val isVisibleOnScreen = MutableStateFlow(false)
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 addFundsSheetNavigation = SlotNavigation<AddFundsSlotRoute>()

View file

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

View file

@ -28,11 +28,13 @@ internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
relatedNews: RelatedNews,
marketingBanner: @Composable (Modifier) -> Unit,
) {
if (isRedesignEnabled) {
tokenMarketDetailsBodyV2(
state = state,
relatedNews = relatedNews,
marketingBanner = marketingBanner,
)
} else {
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
private fun LazyListScope.sectionStub(key: String) {
item(key) { }
}
@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) {
MarketsTokenDetailsUM.Body.Loading -> {
item("description-loading") {
@ -115,6 +132,8 @@ private fun LazyListScope.tokenMarketDetailsBodyV2(state: MarketsTokenDetailsUM.
description(state.description)
}
marketingBannerItem(marketingBanner)
infoBlocksListV2(
state = state.infoBlocks,
relatedNews = relatedNews,
@ -169,7 +188,7 @@ private fun LazyListScope.description(description: MarketsTokenDetailsUM.Descrip
modifier = {
this
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x8)
.padding(bottom = 24.dp)
},
otherModifier = {
blockPaddings()

View file

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

View file

@ -23,6 +23,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.YieldSupplyTestTags
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.impl.R
import com.tangem.features.yield.supply.impl.active.model.YieldSupplyActiveModel
@ -40,6 +41,7 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyActiveComponent.Params,
private val appRouter: AppRouter,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : YieldSupplyActiveComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyActiveModel = getOrCreateModel(params = params)
@ -51,6 +53,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(
key = "yieldSupplyActiveStack",
source = model.slotNavigation,
@ -85,6 +95,7 @@ internal class DefaultYieldSupplyActiveComponent @AssistedInject constructor(
state = state,
isBalanceHidden = isBalanceHidden,
chartComponent = chartComponent,
marketingBannerComponent = marketingBannerComponent,
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.wallets.usecase.GetUserWalletUseCase
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.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.usecase.GetYieldBoostStatusUseCase
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.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
@ -96,6 +101,15 @@ internal class YieldSupplyActiveModel @Inject constructor(
private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency
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>
field = MutableStateFlow(
YieldSupplyActiveContentUM(
@ -145,6 +159,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() {
if (!transactionInProgressFlow.value) {
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.res.TangemTheme
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.active.entity.YieldSupplyActiveContentUM
import kotlinx.collections.immutable.persistentListOf
@ -51,6 +52,7 @@ internal fun YieldSupplyActiveContent(
isBalanceHidden: Boolean,
onReadMoreClick: () -> Unit,
chartComponent: ComposableContentComponent,
marketingBannerComponent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
Column(
@ -88,6 +90,10 @@ internal fun YieldSupplyActiveContent(
}
}
TangemThemeRedesign {
marketingBannerComponent.Content(Modifier.fillMaxWidth())
}
AnimatedVisibility(state.notifications.isNotEmpty()) {
val notifications = remember(state.notifications) { state.notifications }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
@ -418,6 +424,7 @@ private fun YieldSupplyActiveBottomSheet_Preview(
state = params,
isBalanceHidden = true,
chartComponent = ComposableContentComponent.EMPTY,
marketingBannerComponent = ComposableContentComponent.EMPTY,
onReadMoreClick = {},
)
}