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

@ -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.TransferBottomSheetComponent
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.rating.RatingComponent
import com.tangem.features.tokendetails.ExpressTransactionsComponent
@ -54,6 +55,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
private val ratingComponentFactory: RatingComponent.Factory,
marketingBannerComponentFactory: MarketingBannerComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
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
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
@ -153,6 +163,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent,
ratingComponent = ratingSlotState.child?.instance,
marketingBannerComponent = marketingBannerComponent,
modifier = modifier,
)
} else {
@ -164,6 +175,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
yieldSupplyComponent = yieldSupplyComponent,
expressTransactionsComponent = expressTransactionsComponent,
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.routing.AppRoute
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.mapToAddressModels
import com.tangem.features.marketing.api.MarketingBannerRequest
import com.tangem.features.rating.RatingComponent
import com.tangem.feature.swap.domain.SwapFeedbackUseCase
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.feedback.SendBackupProblemEmailUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.marketing.models.MarketingScreen
import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
@ -192,6 +196,16 @@ internal class TokenDetailsModel @Inject constructor(
private val userWalletId: UserWalletId = params.userWalletId
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()
?: error("UserWallet not found")
@ -208,6 +222,7 @@ internal class TokenDetailsModel @Inject constructor(
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var account: Account.CryptoPortfolio? = null
private var isBalanceLoadedEventSent = false
private var latestTokenActions: List<TokenActionsState.ActionState> = emptyList()
val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation()
val ratingSlotNavigation = SlotNavigation<RatingComponent.Params>()
@ -346,6 +361,7 @@ internal class TokenDetailsModel @Inject constructor(
.conflate()
.distinctUntilChanged()
.onEach { state ->
latestTokenActions = state.states
sendButtonsEvents(state.states)
uiState.value = stateFactory.getManageButtonsState(actions = state.states)
if (designFeatureToggles.isRedesignEnabled) {
@ -818,6 +834,29 @@ internal class TokenDetailsModel @Inject constructor(
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) {
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.ZeroBalanceActionsBlock
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.tokendetails.ExpressTransactionsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
@ -73,6 +74,7 @@ internal fun TokenDetailsScreen(
txHistoryComponent: TxHistoryComponent,
expressTransactionsComponent: ExpressTransactionsComponent,
ratingComponent: RatingComponent?,
marketingBannerComponent: MarketingBannerComponent,
modifier: Modifier = Modifier,
) {
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
@ -100,6 +102,7 @@ internal fun TokenDetailsScreen(
txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent,
expressTransactionsToDisplay = expressState.transactionsToDisplay,
marketingBannerComponent = marketingBannerComponent,
rootBackground = rootBackground,
topContentPadding = topBarTotalHeight,
bottomContentPadding = effectiveBottomPadding,
@ -161,6 +164,7 @@ private fun TokenDetailsBody(
txHistoryComponent: TxHistoryComponent,
expressTransactionsComponent: ExpressTransactionsComponent,
expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>,
marketingBannerComponent: MarketingBannerComponent,
rootBackground: Color,
topContentPadding: 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) {
item(key = "zero_balance_actions") {
ZeroBalanceActionsBlock(
@ -317,6 +324,10 @@ private fun TokenDetailsScreen_Preview() {
},
expressTransactionsComponent = PreviewExpressTransactionsComponent,
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.staking.TokenStakingBlockLegacy
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.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
@ -54,6 +55,7 @@ internal fun TokenDetailsScreenLegacy(
yieldSupplyComponent: YieldSupplyComponent,
expressTransactionsComponent: ExpressTransactionsComponent,
ratingComponent: RatingComponent?,
marketingBannerComponent: MarketingBannerComponent,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
@ -154,6 +156,12 @@ internal fun TokenDetailsScreenLegacy(
yieldSupplyComponent.Content(modifier = itemModifier)
}
item(key = "marketing_banner_block") {
TangemThemeRedesign {
marketingBannerComponent.Content(modifier = itemModifier)
}
}
with(expressTransactionsComponent) {
expressTransactionsContentLegacy(
state = expressState.transactionsToDisplay,
@ -215,6 +223,10 @@ private fun TokenDetailsScreenPreview(
},
expressTransactionsComponent = PreviewExpressTransactionsComponent,
ratingComponent = null,
marketingBannerComponent = object : MarketingBannerComponent {
@Composable
override fun Content(modifier: Modifier) = Unit
},
)
}
}