Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-24 22:07:42 +02:00
parent a9916d2702
commit fb08f7c4e1
2 changed files with 166 additions and 145 deletions

View file

@ -45,14 +45,15 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
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.marketing.api.MarketingBannerComponent 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.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
import com.tangem.features.txhistory.entity.TxHistoryItemsUM import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.YieldSupplyComponent
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -81,6 +82,7 @@ internal fun TokenDetailsScreen(
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
val topBarTotalHeight = TopBarHeight + statusBarHeight val topBarTotalHeight = TopBarHeight + statusBarHeight
val hazeState = rememberHazeState() val hazeState = rememberHazeState()
val buttonsHazeState = rememberHazeState()
val rootBackground = TangemTheme.colors2.surface.level2 val rootBackground = TangemTheme.colors2.surface.level2
var marketBlockHeight by remember { mutableStateOf(0.dp) } var marketBlockHeight by remember { mutableStateOf(0.dp) }
@ -92,6 +94,13 @@ internal fun TokenDetailsScreen(
.fillMaxSize() .fillMaxSize()
.background(rootBackground), .background(rootBackground),
) { ) {
Box(
modifier = Modifier
.fillMaxSize()
.background(rootBackground)
.hazeSourceTangem(state = buttonsHazeState, zIndex = -1f),
)
TangemPullToRefreshSlidingContainer( TangemPullToRefreshSlidingContainer(
config = tokenDetailsUM.pullToRefreshConfig, config = tokenDetailsUM.pullToRefreshConfig,
indicatorOffset = topBarTotalHeight, indicatorOffset = topBarTotalHeight,
@ -104,6 +113,7 @@ internal fun TokenDetailsScreen(
expressTransactionsToDisplay = expressState.transactionsToDisplay, expressTransactionsToDisplay = expressState.transactionsToDisplay,
marketingBannerComponent = marketingBannerComponent, marketingBannerComponent = marketingBannerComponent,
rootBackground = rootBackground, rootBackground = rootBackground,
buttonsHazeState = buttonsHazeState,
topContentPadding = topBarTotalHeight, topContentPadding = topBarTotalHeight,
bottomContentPadding = effectiveBottomPadding, bottomContentPadding = effectiveBottomPadding,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -166,6 +176,7 @@ private fun TokenDetailsBody(
expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>, expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>,
marketingBannerComponent: MarketingBannerComponent, marketingBannerComponent: MarketingBannerComponent,
rootBackground: Color, rootBackground: Color,
buttonsHazeState: HazeState,
topContentPadding: Dp, topContentPadding: Dp,
bottomContentPadding: Dp, bottomContentPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -194,11 +205,13 @@ private fun TokenDetailsBody(
contentPadding = PaddingValues(top = topContentPadding, bottom = bottomContentPadding), contentPadding = PaddingValues(top = topContentPadding, bottom = bottomContentPadding),
) { ) {
item(key = "balance_block") { item(key = "balance_block") {
TokenDetailsBalanceBlock( CompositionLocalProvider(LocalHazeState provides buttonsHazeState) {
balanceBlockUM = tokenDetailsUM.balanceBlockUM, TokenDetailsBalanceBlock(
isBalanceHidden = tokenDetailsUM.isBalanceHidden, balanceBlockUM = tokenDetailsUM.balanceBlockUM,
modifier = Modifier.fillMaxWidth(), isBalanceHidden = tokenDetailsUM.isBalanceHidden,
) modifier = Modifier.fillMaxWidth(),
)
}
} }
val balance = tokenDetailsUM.balanceBlockUM val balance = tokenDetailsUM.balanceBlockUM
notifications( notifications(

View file

@ -52,7 +52,6 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggab
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.containers.pullToRefresh.getPullToRefreshIndicatorOffset import com.tangem.core.ui.components.containers.pullToRefresh.getPullToRefreshIndicatorOffset
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.rememberIsKeyboardVisible
import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.sheetscaffold.*
@ -79,8 +78,7 @@ import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.rememberHazeState
import dev.chrisbanes.haze.HazeTint
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -90,6 +88,9 @@ import kotlin.math.abs
private const val MARKET_HINT_THRESHOLD = 0.5f private const val MARKET_HINT_THRESHOLD = 0.5f
// collapsedFraction below this is treated as "fully expanded" (user is at the very top of the list)
private const val FULLY_EXPANDED_THRESHOLD = 0.01f
@OptIn(ExperimentalDecomposeApi::class) @OptIn(ExperimentalDecomposeApi::class)
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
@ -231,161 +232,168 @@ private fun WalletContent2(
val canPagerScroll by remember { derivedStateOf { behavior.state.heightOffset == 0f } } val canPagerScroll by remember { derivedStateOf { behavior.state.heightOffset == 0f } }
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberPullToRefreshState()
val wallpaperHazeState = rememberHazeState()
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.hazeSourceTangem(zIndex = -2f), .hazeSourceTangem(zIndex = -2f),
) { ) {
Box(
modifier = Modifier
.matchParentSize()
.background(TangemTheme.colors3.bg.primary),
)
val isSheetExpanded by remember { val isSheetExpanded by remember {
derivedStateOf { bottomSheetState.targetValue == TangemSheetValue.Expanded } derivedStateOf { bottomSheetState.targetValue == TangemSheetValue.Expanded }
} }
if (!isSheetExpanded) {
NorthernLightsBackground(
containerColor = TangemTheme.colors3.bg.primary,
modifier = Modifier
.graphicsLayer { alpha = 1 - behavior.state.collapsedFraction * 2 }
.matchParentSize(),
)
}
WalletPagerIndicator(
pagerState = walletsPagerState,
pullToRefreshState = pullToRefreshState,
pullToRefreshConfig = pullToRefreshConfig,
behavior = behavior,
topOffset = subtitleBottom + 8.dp,
)
val overlay = TangemTheme.colors3.material.tint.solid
// Root-coordinates bounds of the "Add & Manage" button per pager page, reported by the
// button itself, so the markets hint and tooltip can avoid covering it
val organizeButtonBounds = remember { mutableStateMapOf<Int, Rect>() } val organizeButtonBounds = remember { mutableStateMapOf<Int, Rect>() }
CompositionLocalProvider(LocalHazeState provides wallpaperHazeState) {
HorizontalPager( Box(
state = walletsPagerState,
userScrollEnabled = canPagerScroll,
beyondViewportPageCount = 1,
modifier = Modifier.hazeEffectTangem {
fallbackTint = HazeTint(color = overlay)
progressive = HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 1f,
preferPerformance = true,
)
},
) { currentWalletIndex ->
val listState = listStates[currentWalletIndex] ?: rememberLazyListState()
val currentWallet = state.wallets2.getOrElse(currentWalletIndex) {
state.wallets2[state.selectedWalletIndex]
}
val currentWalletId = currentWallet.walletsBalanceUM.id.stringValue
LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) {
if (walletsPagerState.currentPage == currentWalletIndex) {
walletBalance =
(currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar
}
}
LaunchedEffect(walletsPagerState.currentPage, currentWallet.pullToRefreshConfig) {
if (walletsPagerState.currentPage == currentWalletIndex) {
pullToRefreshConfig = currentWallet.pullToRefreshConfig
}
}
val isShowMarketsHint by remember {
derivedStateOf {
behavior.state.collapsedFraction > MARKET_HINT_THRESHOLD &&
listState.layoutInfo.totalItemsCount > 0 &&
!listState.canScrollBackward && !listState.canScrollForward ||
listState.canScrollBackward && !listState.canScrollForward
}
}
val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex)
val pullToRefreshContentOffset = getPullToRefreshIndicatorOffset(
pullToRefreshConfig = currentWallet.pullToRefreshConfig,
pullToRefreshState = pullToRefreshState,
)
TangemSharedTransitionLayout(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.alpha(pageSlideAlpha), .hazeSourceTangem(zIndex = -1f),
) { ) {
TangemPullToRefreshSlidingContainer( Box(
state = pullToRefreshState, modifier = Modifier
config = currentWallet.pullToRefreshConfig, .fillMaxSize()
indicatorOffset = with(LocalDensity.current) { .background(TangemTheme.colors3.bg.primary),
behavior.state.partialHeightLimit.toDp() )
}, if (!isSheetExpanded) {
) { NorthernLightsBackground(
TangemCollapsingTopBar( containerColor = TangemTheme.colors3.bg.primary,
state = behavior.state, modifier = Modifier
collapsingPart = { .graphicsLayer { alpha = 1 - behavior.state.collapsedFraction * 2 }
val balanceBlockHeight = with(LocalDensity.current) { .fillMaxSize(),
-behavior.state.heightOffsetLimit.toDp() )
}
}
WalletPagerIndicator(
pagerState = walletsPagerState,
pullToRefreshState = pullToRefreshState,
pullToRefreshConfig = pullToRefreshConfig,
behavior = behavior,
topOffset = subtitleBottom + 8.dp,
)
HorizontalPager(
state = walletsPagerState,
userScrollEnabled = canPagerScroll,
beyondViewportPageCount = 1,
) { currentWalletIndex ->
val listState = listStates[currentWalletIndex] ?: rememberLazyListState()
val currentWallet = state.wallets2.getOrElse(currentWalletIndex) {
state.wallets2[state.selectedWalletIndex]
}
val currentWalletId = currentWallet.walletsBalanceUM.id.stringValue
LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) {
if (walletsPagerState.currentPage == currentWalletIndex) {
walletBalance =
(currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar
}
}
LaunchedEffect(walletsPagerState.currentPage, currentWallet.pullToRefreshConfig) {
if (walletsPagerState.currentPage == currentWalletIndex) {
pullToRefreshConfig = currentWallet.pullToRefreshConfig
}
}
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.totalItemsCount }
.collectLatest {
if (behavior.state.collapsedFraction < FULLY_EXPANDED_THRESHOLD &&
listState.firstVisibleItemIndex != 0
) {
listState.scrollToItem(index = 0)
} }
WalletBalance( }
behavior = behavior, }
walletBalanceUM = currentWallet.walletsBalanceUM,
buttons = currentWallet.buttons, val isShowMarketsHint by remember {
isBalanceHidden = state.isHidingMode, derivedStateOf {
modifier = Modifier.height(balanceBlockHeight), behavior.state.collapsedFraction > MARKET_HINT_THRESHOLD &&
onSubtitleBottomChange = { newValue -> listState.layoutInfo.totalItemsCount > 0 &&
if (pullToRefreshContentOffset == 0.dp && newValue > subtitleBottom) { !listState.canScrollBackward && !listState.canScrollForward ||
subtitleBottom = newValue listState.canScrollBackward && !listState.canScrollForward
} }
}, }
)
val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex)
val pullToRefreshContentOffset = getPullToRefreshIndicatorOffset(
pullToRefreshConfig = currentWallet.pullToRefreshConfig,
pullToRefreshState = pullToRefreshState,
)
TangemSharedTransitionLayout(
modifier = Modifier
.fillMaxSize()
.alpha(pageSlideAlpha),
) {
TangemPullToRefreshSlidingContainer(
state = pullToRefreshState,
config = currentWallet.pullToRefreshConfig,
indicatorOffset = with(LocalDensity.current) {
behavior.state.partialHeightLimit.toDp()
}, },
body = { ) {
WalletListContent( TangemCollapsingTopBar(
currentWallet = currentWallet, state = behavior.state,
listState = listState, collapsingPart = {
isBalanceHidden = state.isHidingMode, val balanceBlockHeight = with(LocalDensity.current) {
contentPadding = contentPadding, -behavior.state.heightOffsetLimit.toDp()
tangemPayComponent = tangemPayComponent, }
promoBannersBlockComponent = promoBannersBlockComponent, WalletBalance(
walletId = currentWalletId, behavior = behavior,
virtualAccountComponent = virtualAccountComponent, walletBalanceUM = currentWallet.walletsBalanceUM,
onOrganizeButtonBoundsChange = remember(currentWalletIndex) { buttons = currentWallet.buttons,
{ bounds -> isBalanceHidden = state.isHidingMode,
if (bounds != null) { modifier = Modifier.height(balanceBlockHeight),
organizeButtonBounds[currentWalletIndex] = bounds onSubtitleBottomChange = { newValue ->
} else { if (pullToRefreshContentOffset == 0.dp && newValue > subtitleBottom) {
organizeButtonBounds.remove(currentWalletIndex) subtitleBottom = newValue
} }
} },
}, )
modifier = Modifier },
.fillMaxSize() body = {
.nestedScroll(behavior.nestedScrollConnection), WalletListContent(
) currentWallet = currentWallet,
listState = listState,
isBalanceHidden = state.isHidingMode,
contentPadding = contentPadding,
tangemPayComponent = tangemPayComponent,
promoBannersBlockComponent = promoBannersBlockComponent,
walletId = currentWalletId,
virtualAccountComponent = virtualAccountComponent,
onOrganizeButtonBoundsChange = remember(currentWalletIndex) {
{ bounds ->
if (bounds != null) {
organizeButtonBounds[currentWalletIndex] = bounds
} else {
organizeButtonBounds.remove(currentWalletIndex)
}
}
},
modifier = Modifier
.fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection),
)
},
)
}
MarketsHint(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(fraction = .6f)
.padding(bottom = paddingValues.calculateBottomPadding())
.onSizeChanged { size ->
marketsHintHeight = with(density) { size.height.toDp() }
},
isVisible = isShowMarketsHint,
obstacleBounds = remember(currentWalletIndex) {
{ organizeButtonBounds[currentWalletIndex] }
}, },
) )
} }
MarketsHint(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(fraction = .6f)
.padding(bottom = paddingValues.calculateBottomPadding())
.onSizeChanged { size ->
marketsHintHeight = with(density) { size.height.toDp() }
},
isVisible = isShowMarketsHint,
obstacleBounds = remember(currentWalletIndex) {
{ organizeButtonBounds[currentWalletIndex] }
},
)
} }
} }