Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 08:12:11 +02:00
parent 18da968728
commit aba2f43c34
11 changed files with 224 additions and 4 deletions

View file

@ -68,6 +68,7 @@ dependencies {
implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.models)
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.account.status) implementation(projects.domain.account.status)
implementation(projects.domain.marketing.models)
/** Common */ /** Common */
implementation(projects.common.ui) implementation(projects.common.ui)
@ -80,6 +81,7 @@ dependencies {
implementation(projects.features.staking.api) implementation(projects.features.staking.api)
implementation(projects.features.txhistory.api) implementation(projects.features.txhistory.api)
implementation(projects.features.approval.api) implementation(projects.features.approval.api)
implementation(projects.features.marketing.api)
/** Decompose */ /** Decompose */
implementation(deps.decompose.ext.compose) implementation(deps.decompose.ext.compose)

View file

@ -7,9 +7,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.staking.impl.presentation.model.StakingModel import com.tangem.features.staking.impl.presentation.model.StakingModel
import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.ui.StakingScreen
@ -21,10 +23,19 @@ internal class DefaultStakingComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted params: StakingComponent.Params, @Assisted params: StakingComponent.Params,
private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, private val giveApprovalComponentFactory: GiveApprovalComponent.Factory,
private val marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : StakingComponent, AppComponentContext by appComponentContext { ) : StakingComponent, AppComponentContext by appComponentContext {
private val model: StakingModel = getOrCreateModel(params) private val model: StakingModel = getOrCreateModel(params)
private val marketingBannerComponent: MarketingBannerComponent = marketingBannerComponentFactory.create(
context = child("stakingMarketingBanner"),
params = MarketingBannerComponent.Params.Standalone(
requestFlow = model.marketingRequest,
onDeeplinkClick = model::onMarketingBannerDeeplink,
),
)
private val approvalSlot = childSlot( private val approvalSlot = childSlot(
key = "stakingApprovalSlot", key = "stakingApprovalSlot",
source = model.approvalSlotNavigation, source = model.approvalSlotNavigation,
@ -45,7 +56,7 @@ internal class DefaultStakingComponent @AssistedInject constructor(
val currentState by model.uiState.collectAsStateWithLifecycle() val currentState by model.uiState.collectAsStateWithLifecycle()
val approvalSlotState by approvalSlot.subscribeAsState() val approvalSlotState by approvalSlot.subscribeAsState()
StakingScreen(currentState) StakingScreen(currentState, marketingBannerComponent)
approvalSlotState.child?.instance?.BottomSheet() approvalSlotState.child?.instance?.BottomSheet()
} }

View file

@ -10,6 +10,8 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.getValidatorsCount import com.tangem.common.getValidatorsCount
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.resolveMarketingDeeplink
import com.tangem.common.routing.deeplink.toContextualRoute
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
@ -42,6 +44,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -67,6 +70,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.marketing.api.MarketingBannerRequest
import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor
@ -153,7 +157,7 @@ internal class StakingModel @Inject constructor(
private val innerRouter: InnerStakingRouter, private val innerRouter: InnerStakingRouter,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val stakingFeatureToggles: StakingFeatureToggles, private val stakingFeatureToggles: StakingFeatureToggles,
appRouter: AppRouter, private val appRouter: AppRouter,
) : Model(), StakingClickIntents { ) : Model(), StakingClickIntents {
val uiState: StateFlow<StakingUiState> = stateController.uiState val uiState: StateFlow<StakingUiState> = stateController.uiState
@ -163,6 +167,15 @@ internal class StakingModel @Inject constructor(
private val params = paramsContainer.require<StakingComponent.Params>() private val params = paramsContainer.require<StakingComponent.Params>()
val marketingRequest: Flow<MarketingBannerRequest?> = flowOf(
MarketingBannerRequest(
screen = MarketingScreen.Staking(
networkId = params.cryptoCurrency.network.rawId,
contractAddress = (params.cryptoCurrency as? CryptoCurrency.Token)?.contractAddress.orEmpty(),
),
),
)
private val stakingStateRouter: StakingStateRouter = StakingStateRouter( private val stakingStateRouter: StakingStateRouter = StakingStateRouter(
appRouter = appRouter, appRouter = appRouter,
stateController = stateController, stateController = stateController,
@ -317,6 +330,16 @@ internal class StakingModel @Inject constructor(
stateController.initializeWithUserWallet(userWallet) stateController.initializeWithUserWallet(userWallet)
} }
fun onMarketingBannerDeeplink(deeplink: String): Boolean {
val route = resolveMarketingDeeplink(deeplink).toContextualRoute(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
screenSource = AnalyticsParam.ScreensSources.Staking,
) ?: return false
appRouter.push(route)
return true
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
paramsInterceptorHolder.removeParamsInterceptor(StakingParamsInterceptor.ID) paramsInterceptorHolder.removeParamsInterceptor(StakingParamsInterceptor.ID)

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
@ -51,6 +52,7 @@ import com.tangem.core.ui.test.StakingDetailsScreenTestTags
import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.staking.model.common.RewardType import com.tangem.domain.staking.model.common.RewardType
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -67,6 +69,7 @@ private const val BANNER_BLOCK_KEY = "BannerBlock"
private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock"
private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock"
private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton" private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton"
private const val MARKETING_BANNER_BLOCK_KEY = "MarketingBannerBlock"
@Composable @Composable
internal fun StakingInitialInfoContent( internal fun StakingInitialInfoContent(
@ -74,6 +77,7 @@ internal fun StakingInitialInfoContent(
buttonState: NavigationButtonsState, buttonState: NavigationButtonsState,
clickIntents: StakingClickIntents, clickIntents: StakingClickIntents,
isBalanceHidden: Boolean, isBalanceHidden: Boolean,
marketingBannerComponent: MarketingBannerComponent,
) { ) {
if (state !is StakingStates.InitialInfoState.Data) return if (state !is StakingStates.InitialInfoState.Data) return
@ -104,6 +108,10 @@ internal fun StakingInitialInfoContent(
hideEndText = isBalanceHidden, hideEndText = isBalanceHidden,
) )
item(key = MARKETING_BANNER_BLOCK_KEY) {
marketingBannerComponent.Content(Modifier.fillMaxWidth().padding(bottom = 12.dp))
}
activeStakingBlock( activeStakingBlock(
state = state, state = state,
clickIntents = clickIntents, clickIntents = clickIntents,
@ -491,6 +499,10 @@ private fun StakingInitialInfoContent_Preview(
buttonState = NavigationButtonsState.Empty, buttonState = NavigationButtonsState.Empty,
clickIntents = StakingClickIntentsStub, clickIntents = StakingClickIntentsStub,
isBalanceHidden = false, isBalanceHidden = false,
marketingBannerComponent = object : MarketingBannerComponent {
@Composable
override fun Content(modifier: Modifier) = Unit
},
) )
} }
} }

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendScreenTestTags import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingStep
@ -35,7 +36,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.flow.withIndex
@Composable @Composable
internal fun StakingScreen(uiState: StakingUiState) { internal fun StakingScreen(uiState: StakingUiState, marketingBannerComponent: MarketingBannerComponent) {
val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data
BackHandler(onBack = uiState.clickIntents::onPrevClick) BackHandler(onBack = uiState.clickIntents::onPrevClick)
@ -53,6 +54,7 @@ internal fun StakingScreen(uiState: StakingUiState) {
) )
StakingScreenContent( StakingScreenContent(
uiState = uiState, uiState = uiState,
marketingBannerComponent = marketingBannerComponent,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
NavigationButtonsBlock( NavigationButtonsBlock(
@ -104,7 +106,11 @@ private fun StakingAppBar(uiState: StakingUiState) {
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { private fun StakingScreenContent(
uiState: StakingUiState,
marketingBannerComponent: MarketingBannerComponent,
modifier: Modifier = Modifier,
) {
val currentScreen = uiState.currentStep val currentScreen = uiState.currentStep
var currentStateProxy by remember { mutableStateOf(currentScreen) } var currentStateProxy by remember { mutableStateOf(currentScreen) }
var isTransitionAnimationRunning by remember { mutableStateOf(false) } var isTransitionAnimationRunning by remember { mutableStateOf(false) }
@ -149,6 +155,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
buttonState = uiState.buttonsState, buttonState = uiState.buttonsState,
clickIntents = uiState.clickIntents, clickIntents = uiState.clickIntents,
isBalanceHidden = uiState.isBalanceHidden, isBalanceHidden = uiState.isBalanceHidden,
marketingBannerComponent = marketingBannerComponent,
) )
StakingStep.RewardsValidators -> { StakingStep.RewardsValidators -> {
StakingClaimRewardsValidatorContent( StakingClaimRewardsValidatorContent(

View file

@ -0,0 +1,89 @@
package com.tangem.features.staking.impl.presentation.model
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.onramp.model.OnrampSource
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class StakingModelMarketingDeeplinkTest : StakingModelTestBase() {
@Test
fun `GIVEN swap deeplink WHEN onMarketingBannerDeeplink THEN pushes Swap for current token`() = runTest {
// Arrange
every { appRouter.push(any(), any()) } just Runs
val model = createModel(testScope = this)
advanceUntilIdle()
// Act
val handled = model.onMarketingBannerDeeplink("tangem://swap")
// Assert
assertThat(handled).isTrue()
verify {
appRouter.push(
match {
it is AppRoute.Swap &&
it.userWalletId == testUserWalletId &&
it.fromCryptoCurrency == testCryptoCurrency &&
it.screenSource == AnalyticsParam.ScreensSources.Staking.value
},
any(),
)
}
model.onDestroy()
}
@Test
fun `GIVEN buy deeplink WHEN onMarketingBannerDeeplink THEN pushes Onramp for current token`() = runTest {
// Arrange
every { appRouter.push(any(), any()) } just Runs
val model = createModel(testScope = this)
advanceUntilIdle()
// Act
val handled = model.onMarketingBannerDeeplink("tangem://buy")
// Assert
assertThat(handled).isTrue()
verify {
appRouter.push(
match {
it is AppRoute.Onramp &&
it.userWalletId == testUserWalletId &&
it.currency == testCryptoCurrency &&
it.source == OnrampSource.MARKETING_BANNER
},
any(),
)
}
model.onDestroy()
}
@Test
fun `GIVEN external deeplink WHEN onMarketingBannerDeeplink THEN not handled and no navigation`() = runTest {
// Arrange
every { appRouter.push(any(), any()) } just Runs
val model = createModel(testScope = this)
advanceUntilIdle()
// Act
val handled = model.onMarketingBannerDeeplink("https://tangem.com/promo")
// Assert
assertThat(handled).isFalse()
verify(exactly = 0) { appRouter.push(any(), any()) }
model.onDestroy()
}
}

View file

@ -70,6 +70,7 @@ dependencies {
implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.dynamicAddresses.models)
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.markets.models) implementation(projects.domain.markets.models)
implementation(projects.domain.marketing.models)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.models)
implementation(projects.domain.offramp) implementation(projects.domain.offramp)
@ -101,6 +102,7 @@ dependencies {
implementation(projects.features.wallet.api) implementation(projects.features.wallet.api)
implementation(projects.features.staking.api) implementation(projects.features.staking.api)
implementation(projects.features.markets.api) implementation(projects.features.markets.api)
implementation(projects.features.marketing.api)
implementation(projects.features.onramp.api) implementation(projects.features.onramp.api)
implementation(projects.features.pushNotifications.api) implementation(projects.features.pushNotifications.api)
implementation(projects.features.swap.api) implementation(projects.features.swap.api)

View file

@ -27,6 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.rating.RatingComponent import com.tangem.features.rating.RatingComponent
import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent
@ -54,6 +55,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
private val manageFundsComponentFactory: ManageFundsComponent.Factory, private val manageFundsComponentFactory: ManageFundsComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory, yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
private val ratingComponentFactory: RatingComponent.Factory, private val ratingComponentFactory: RatingComponent.Factory,
marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext { ) : TokenDetailsComponent, AppComponentContext by appComponentContext {
private val model: TokenDetailsModel = getOrCreateModel(params) private val model: TokenDetailsModel = getOrCreateModel(params)
@ -136,6 +138,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
), ),
) )
private val marketingBannerComponent = marketingBannerComponentFactory.create(
context = child("tokenDetailsMarketingBanner"),
params = MarketingBannerComponent.Params.Standalone(
requestFlow = model.marketingRequest,
onDeeplinkClick = model::onMarketingBannerDeeplink,
),
)
@Composable @Composable
override fun Content(modifier: Modifier) { override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState() val bottomSheet by bottomSheetSlot.subscribeAsState()
@ -153,6 +163,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
txHistoryComponent = txHistoryComponent, txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent, expressTransactionsComponent = expressTransactionsComponent,
ratingComponent = ratingSlotState.child?.instance, ratingComponent = ratingSlotState.child?.instance,
marketingBannerComponent = marketingBannerComponent,
modifier = modifier, modifier = modifier,
) )
} else { } else {
@ -164,6 +175,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
yieldSupplyComponent = yieldSupplyComponent, yieldSupplyComponent = yieldSupplyComponent,
expressTransactionsComponent = expressTransactionsComponent, expressTransactionsComponent = expressTransactionsComponent,
ratingComponent = ratingSlotState.child?.instance, ratingComponent = ratingSlotState.child?.instance,
marketingBannerComponent = marketingBannerComponent,
) )
} }

View file

@ -12,8 +12,11 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.MarketingDeeplink
import com.tangem.common.routing.deeplink.resolveMarketingDeeplink
import com.tangem.common.ui.bottomsheet.receive.AddressModel import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.features.marketing.api.MarketingBannerRequest
import com.tangem.features.rating.RatingComponent import com.tangem.features.rating.RatingComponent
import com.tangem.feature.swap.domain.SwapFeedbackUseCase import com.tangem.feature.swap.domain.SwapFeedbackUseCase
import com.tangem.feature.swap.domain.models.domain.SwapFeedbackParams import com.tangem.feature.swap.domain.models.domain.SwapFeedbackParams
@ -58,6 +61,7 @@ import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.models.TokenReceiveNotification import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
@ -192,6 +196,16 @@ internal class TokenDetailsModel @Inject constructor(
private val userWalletId: UserWalletId = params.userWalletId private val userWalletId: UserWalletId = params.userWalletId
private val cryptoCurrency: CryptoCurrency = params.currency private val cryptoCurrency: CryptoCurrency = params.currency
/** Token details context is static (single currency) — no amount filter. */
val marketingRequest: Flow<MarketingBannerRequest?> = flowOf(
MarketingBannerRequest(
screen = MarketingScreen.TokenDetails(
networkId = cryptoCurrency.network.rawId,
contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress.orEmpty(),
),
),
)
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull()
?: error("UserWallet not found") ?: error("UserWallet not found")
@ -208,6 +222,7 @@ internal class TokenDetailsModel @Inject constructor(
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var account: Account.CryptoPortfolio? = null private var account: Account.CryptoPortfolio? = null
private var isBalanceLoadedEventSent = false private var isBalanceLoadedEventSent = false
private var latestTokenActions: List<TokenActionsState.ActionState> = emptyList()
val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation()
val ratingSlotNavigation = SlotNavigation<RatingComponent.Params>() val ratingSlotNavigation = SlotNavigation<RatingComponent.Params>()
@ -346,6 +361,7 @@ internal class TokenDetailsModel @Inject constructor(
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { state -> .onEach { state ->
latestTokenActions = state.states
sendButtonsEvents(state.states) sendButtonsEvents(state.states)
uiState.value = stateFactory.getManageButtonsState(actions = state.states) uiState.value = stateFactory.getManageButtonsState(actions = state.states)
if (designFeatureToggles.isRedesignEnabled) { if (designFeatureToggles.isRedesignEnabled) {
@ -818,6 +834,29 @@ internal class TokenDetailsModel @Inject constructor(
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.ANY, checkYieldSupply = true) handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.ANY, checkYieldSupply = true)
} }
/**
* Routes a tapped marketing-banner deeplink contextually for the current token. Reuses the regular
* swap/buy intents (availability checks, yield-supply warning, analytics). Returns `false` for
* external links so the banner falls back to the generic deeplink launcher.
*/
fun onMarketingBannerDeeplink(deeplink: String): Boolean = when (resolveMarketingDeeplink(deeplink)) {
MarketingDeeplink.SWAP -> {
val reason = latestTokenActions
.filterIsInstance<TokenActionsState.ActionState.Swap>()
.firstOrNull()?.unavailabilityReason ?: ScenarioUnavailabilityReason.None
onSwapClick(reason)
true
}
MarketingDeeplink.BUY -> {
val reason = latestTokenActions
.filterIsInstance<TokenActionsState.ActionState.Buy>()
.firstOrNull()?.unavailabilityReason ?: ScenarioUnavailabilityReason.None
onBuyClick(reason)
true
}
MarketingDeeplink.EXTERNAL -> false
}
override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) {
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true) handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true)
} }

View file

@ -46,6 +46,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.ZeroBalanceActionsBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.ZeroBalanceActionsBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.rating.RatingComponent import com.tangem.features.rating.RatingComponent
import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.component.TxHistoryComponent
@ -73,6 +74,7 @@ internal fun TokenDetailsScreen(
txHistoryComponent: TxHistoryComponent, txHistoryComponent: TxHistoryComponent,
expressTransactionsComponent: ExpressTransactionsComponent, expressTransactionsComponent: ExpressTransactionsComponent,
ratingComponent: RatingComponent?, ratingComponent: RatingComponent?,
marketingBannerComponent: MarketingBannerComponent,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle() val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
@ -100,6 +102,7 @@ internal fun TokenDetailsScreen(
txHistoryComponent = txHistoryComponent, txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent, expressTransactionsComponent = expressTransactionsComponent,
expressTransactionsToDisplay = expressState.transactionsToDisplay, expressTransactionsToDisplay = expressState.transactionsToDisplay,
marketingBannerComponent = marketingBannerComponent,
rootBackground = rootBackground, rootBackground = rootBackground,
topContentPadding = topBarTotalHeight, topContentPadding = topBarTotalHeight,
bottomContentPadding = effectiveBottomPadding, bottomContentPadding = effectiveBottomPadding,
@ -161,6 +164,7 @@ private fun TokenDetailsBody(
txHistoryComponent: TxHistoryComponent, txHistoryComponent: TxHistoryComponent,
expressTransactionsComponent: ExpressTransactionsComponent, expressTransactionsComponent: ExpressTransactionsComponent,
expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>, expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>,
marketingBannerComponent: MarketingBannerComponent,
rootBackground: Color, rootBackground: Color,
topContentPadding: Dp, topContentPadding: Dp,
bottomContentPadding: Dp, bottomContentPadding: Dp,
@ -230,6 +234,9 @@ private fun TokenDetailsBody(
) )
} }
} }
item(key = "marketing_banner_block") {
marketingBannerComponent.Content(modifier = itemModifier.padding(vertical = 8.dp))
}
if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) { if (balance is TokenDetailsBalanceBlockUM.Content && balance.isBalanceZero) {
item(key = "zero_balance_actions") { item(key = "zero_balance_actions") {
ZeroBalanceActionsBlock( ZeroBalanceActionsBlock(
@ -317,6 +324,10 @@ private fun TokenDetailsScreen_Preview() {
}, },
expressTransactionsComponent = PreviewExpressTransactionsComponent, expressTransactionsComponent = PreviewExpressTransactionsComponent,
ratingComponent = null, ratingComponent = null,
marketingBannerComponent = object : MarketingBannerComponent {
@Composable
override fun Content(modifier: Modifier) = Unit
},
) )
} }
} }

View file

@ -34,6 +34,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlockLegacy import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlockLegacy
import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryItemsUM import com.tangem.features.txhistory.entity.TxHistoryItemsUM
@ -54,6 +55,7 @@ internal fun TokenDetailsScreenLegacy(
yieldSupplyComponent: YieldSupplyComponent, yieldSupplyComponent: YieldSupplyComponent,
expressTransactionsComponent: ExpressTransactionsComponent, expressTransactionsComponent: ExpressTransactionsComponent,
ratingComponent: RatingComponent?, ratingComponent: RatingComponent?,
marketingBannerComponent: MarketingBannerComponent,
) { ) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
@ -154,6 +156,12 @@ internal fun TokenDetailsScreenLegacy(
yieldSupplyComponent.Content(modifier = itemModifier) yieldSupplyComponent.Content(modifier = itemModifier)
} }
item(key = "marketing_banner_block") {
TangemThemeRedesign {
marketingBannerComponent.Content(modifier = itemModifier)
}
}
with(expressTransactionsComponent) { with(expressTransactionsComponent) {
expressTransactionsContentLegacy( expressTransactionsContentLegacy(
state = expressState.transactionsToDisplay, state = expressState.transactionsToDisplay,
@ -215,6 +223,10 @@ private fun TokenDetailsScreenPreview(
}, },
expressTransactionsComponent = PreviewExpressTransactionsComponent, expressTransactionsComponent = PreviewExpressTransactionsComponent,
ratingComponent = null, ratingComponent = null,
marketingBannerComponent = object : MarketingBannerComponent {
@Composable
override fun Content(modifier: Modifier) = Unit
},
) )
} }
} }