diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt index b9d7920386..2aff7dd2e3 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/tokenselector/TokenSelectorBottomSheet.kt @@ -25,10 +25,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.components.haze.hazeSourceTangem -import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -38,7 +36,7 @@ import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.rememberHazeState @Composable -fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig, stickyFooter: StickyFooter? = null) { +fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, type = TangemBottomSheetType.Modal, @@ -47,56 +45,52 @@ fun TokenSelectorBottomSheet(config: TangemBottomSheetConfig, stickyFooter: Stic TokenSelectorContent( content = content, onDismiss = config.onDismissRequest, - stickyFooter = stickyFooter, embedded = false, ) }, ) } -data class StickyFooter(val buttonText: TextReference, val isEnabled: Boolean = true, val onClick: () -> Unit) - @Composable fun TokenSelectorEmbeddedContent( content: TokenSelectorContentUM, - stickyFooter: StickyFooter?, + scrollBottomInset: Dp, modifier: Modifier = Modifier, ) { TokenSelectorContent( content = content, - onDismiss = {}, - stickyFooter = stickyFooter, embedded = true, modifier = modifier, + scrollBottomInset = scrollBottomInset, ) } @Composable private fun TokenSelectorContent( content: TokenSelectorContentUM, - onDismiss: () -> Unit, - stickyFooter: StickyFooter?, embedded: Boolean, modifier: Modifier = Modifier, + scrollBottomInset: Dp = 0.dp, + onDismiss: () -> Unit = {}, ) { val hazeState = rememberHazeState() var topBarHeight by remember { mutableStateOf(0.dp) } val topContentPadding = if (embedded) { - TangemTheme.dimens2.x4 + 0.dp } else { topBarHeight } - val footerHeight = TangemTheme.dimens2.x14 + TangemTheme.dimens2.x4 - val listBottomPadding = TangemTheme.dimens2.x10 + if (stickyFooter != null) footerHeight else 0.dp Box(modifier = modifier.fillMaxWidth()) { + val bottomFadeReserve = if (embedded) 0.dp else TangemTheme.dimens2.x10 + val bottomListPadding = bottomFadeReserve + scrollBottomInset LazyColumn( modifier = Modifier.hazeSourceTangem(state = hazeState, 1f), contentPadding = PaddingValues( start = TangemTheme.dimens2.x4, end = TangemTheme.dimens2.x4, top = topContentPadding, - bottom = listBottomPadding, + bottom = bottomListPadding, ), ) { tokenSelectorSectionItems(content.sections) @@ -108,29 +102,11 @@ private fun TokenSelectorContent( hazeState = hazeState, onChangeHeight = { topBarHeight = it }, ) - } - Fade( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = if (stickyFooter != null) footerHeight else 0.dp), - height = TangemTheme.dimens2.x10, - ) - if (stickyFooter != null) { - TangemButton( + Fade( modifier = Modifier - .align(Alignment.BottomCenter) - .navigationBarsPadding() - .padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2) - .fillMaxWidth(), - buttonUM = TangemButtonUM( - type = TangemButtonType.Primary, - text = stickyFooter.buttonText, - shape = TangemButtonShape.Rounded, - size = TangemButtonSize.X15, - isEnabled = stickyFooter.isEnabled, - onClick = stickyFooter.onClick, - ), + .fillMaxWidth() + .align(Alignment.BottomCenter), + height = TangemTheme.dimens2.x10, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContentV2.kt b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContentV2.kt index 701d39d6c8..2f5e65b38a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContentV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/addtoken/AddTokenContentV2.kt @@ -191,6 +191,7 @@ private fun AddButton(state: AddTokenUM.Button, modifier: Modifier = Modifier) { isEnabled = state.isEnabled, size = TangemButtonSize.X12, shape = TangemButtonShape.Rounded, + isLoading = state.showProgress, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 60b4d4699a..ce93e4cc75 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -10,10 +10,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -36,6 +37,14 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.utils.WindowInsetsZero +/** + * Extra bottom inset that scrollable content under a [BasicBottomSheet] should reserve so it + * does not collide with the footer overlay: measured footer height plus the bottom gradient + * (see `gradientHeight` in [FooterOverlay]). Equals `0.dp` when there is no footer or when read + * outside [BasicBottomSheet]. + */ +val LocalTangemBottomSheetContentBottomInset = compositionLocalOf { 0.dp } + /** * Type of [TangemBottomSheet] that defines its behavior and appearance. * - [Default]: Standard bottom sheet with a draggable header @@ -204,6 +213,9 @@ inline fun BasicBottomSheet( ) { val model = config.content as? T ?: return val windowSize = LocalWindowSize.current + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + var footerHeightDp by remember { mutableStateOf(null) } val bsContent: @Composable ColumnScope.() -> Unit = { val maxHeight = when (type) { @@ -212,17 +224,19 @@ inline fun BasicBottomSheet( } val contentModifier = when (type) { - Default -> Modifier.clip( - RoundedCornerShape( - topStart = TangemTheme.dimens2.x8, - topEnd = TangemTheme.dimens2.x8, - ), - ) + Default -> Modifier + .padding(bottom = bottomBarHeight) + .clip( + RoundedCornerShape( + topStart = TangemTheme.dimens2.x8, + topEnd = TangemTheme.dimens2.x8, + ), + ) Modal -> Modifier .padding( start = TangemTheme.dimens2.x2, end = TangemTheme.dimens2.x2, - bottom = TangemTheme.dimens2.x2, + bottom = bottomBarHeight, ) .clip(RoundedCornerShape(TangemTheme.dimens2.x8)) } @@ -236,26 +250,19 @@ inline fun BasicBottomSheet( title(model) } Box(modifier = Modifier.fillMaxWidth()) { - content(model) if (footer != null) { - BottomFade( - modifier = Modifier.align(Alignment.BottomCenter), - gradientBrush = Brush.verticalGradient( - listOf( - TangemTheme.colors2.shadow.fadeMin, - TangemTheme.colors2.shadow.fadeMax, - ), - ), + FooterOverlay( + measuredFooterHeight = footerHeightDp, + onMeasureFooter = { newHeight -> + if (newHeight != footerHeightDp) { + footerHeightDp = newHeight + } + }, + footer = { footer(model) }, + content = { content(model) }, ) - } - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - ) { - if (footer != null) { - footer(model) - } + } else { + content(model) } } } @@ -274,6 +281,57 @@ inline fun BasicBottomSheet( ) } +@Composable +fun BoxScope.FooterOverlay( + measuredFooterHeight: Dp?, + onMeasureFooter: (Dp) -> Unit, + footer: @Composable BoxScope.() -> Unit, + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + val gradientHeight = TangemTheme.dimens2.x10 + val isFooterRendered = measuredFooterHeight == null || measuredFooterHeight > 0.dp + val contentBottomOverlayHeight = if (isFooterRendered) { + (measuredFooterHeight ?: 0.dp) + gradientHeight + } else { + 0.dp + } + val fadeMax = TangemTheme.colors2.surface.level2 + CompositionLocalProvider( + LocalTangemBottomSheetContentBottomInset provides contentBottomOverlayHeight, + ) { + content() + } + if (isFooterRendered) { + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) { + Fade( + backgroundColor = fadeMax, + height = gradientHeight, + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(measuredFooterHeight ?: 0.dp) + .background(fadeMax), + ) + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .onGloballyPositioned { coordinates -> + onMeasureFooter(with(density) { coordinates.size.height.toDp() }) + }, + ) { + footer() + } +} + // region Preview @Suppress("LongMethod") @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index bc59df4610..89d58c63d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -46,6 +46,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { color = badgeUM.color, type = badgeUM.type, iconPosition = badgeUM.iconPosition, + shouldRespectIconTint = badgeUM.shouldRespectIconTint, onClick = badgeUM.onClick, modifier = modifier, ) @@ -77,6 +78,7 @@ fun TangemBadge( color: TangemBadgeColor = TangemBadgeColor.Gray, type: TangemBadgeType = TangemBadgeType.Solid, iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None, + shouldRespectIconTint: Boolean = false, onClick: (() -> Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) @@ -95,6 +97,7 @@ fun TangemBadge( iconPosition = iconPosition, size = size, iconColor = iconColor, + shouldRespectIconTint = shouldRespectIconTint, ) AnimatedVisibility( visible = text != null, @@ -113,6 +116,7 @@ fun TangemBadge( iconPosition = iconPosition, size = size, iconColor = iconColor, + shouldRespectIconTint = shouldRespectIconTint, ) } } @@ -123,6 +127,7 @@ private fun StartIcon( size: TangemBadgeSize, iconColor: Color, tangemIconUM: TangemIconUM? = null, + shouldRespectIconTint: Boolean = false, ) { AnimatedVisibility( visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End, @@ -139,7 +144,11 @@ private fun StartIcon( is TangemIconUM.Url, TangemIconUM.Empty, -> wrappedIconRes - is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + is TangemIconUM.Icon -> if (shouldRespectIconTint) { + wrappedIconRes + } else { + wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + } }, ) } @@ -151,6 +160,7 @@ private fun EndIcon( size: TangemBadgeSize, iconColor: Color, tangemIconUM: TangemIconUM? = null, + shouldRespectIconTint: Boolean = false, ) { AnimatedVisibility( visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End, @@ -167,7 +177,11 @@ private fun EndIcon( is TangemIconUM.Url, TangemIconUM.Empty, -> wrappedIconRes - is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + is TangemIconUM.Icon -> if (shouldRespectIconTint) { + wrappedIconRes + } else { + wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + } }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt index 83843fbc3d..84cff62206 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt @@ -7,14 +7,16 @@ import com.tangem.core.ui.extensions.TextReference /** * UI model for [TangemBadge] component * - * @param text TextReference for the badge label. - * @param tangemIconUM Model of representation for the icon to be displayed in the badge. - * @param size [TangemBadgeSize] defining the size of the badge. - * @param shape [TangemBadgeShape] defining the shape of the badge. - * @param color [TangemBadgeColor] defining the color scheme of the badge. - * @param type [TangemBadgeType] defining the style of the badge. - * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. - * @param onClick Lambda to be invoked when the badge is clicked (optional). + * @param text TextReference for the badge label. + * @param tangemIconUM Model of representation for the icon to be displayed in the badge. + * @param size [TangemBadgeSize] defining the size of the badge. + * @param shape [TangemBadgeShape] defining the shape of the badge. + * @param color [TangemBadgeColor] defining the color scheme of the badge. + * @param type [TangemBadgeType] defining the style of the badge. + * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. + * @param shouldRespectIconTint When true, the icon's own tintReference is preserved instead of being overridden by the + * badge color scheme. Useful when the icon carries its own semantic color (e.g. account icons). + * @param onClick Lambda to be invoked when the badge is clicked (optional). */ class TangemBadgeUM( val text: TextReference, @@ -24,5 +26,6 @@ class TangemBadgeUM( val color: TangemBadgeColor = TangemBadgeColor.Gray, val type: TangemBadgeType = TangemBadgeType.Solid, val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + val shouldRespectIconTint: Boolean = false, val onClick: (() -> Unit)? = null, ) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_arrow_down_20.xml b/core/ui/src/main/res/drawable/ic_arrow_down_20.xml new file mode 100644 index 0000000000..f3206755ec --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_down_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetFooter.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetFooter.kt new file mode 100644 index 0000000000..dac734d96d --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetFooter.kt @@ -0,0 +1,110 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalHazeState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioFooterKind +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM +import dev.chrisbanes.haze.rememberHazeState + +@Composable +internal fun AddToPortfolioBottomSheetFooter( + currentRoute: AddToPortfolioRoutes, + userPortfolioState: State?, + onBack: () -> Unit, + onAddFromUserPortfolioClick: (() -> Unit)?, +) { + when (currentRoute.uiSpec().footer) { + AddToPortfolioFooterKind.Cancel -> WithLocalHaze { + CancelFooterButton(onClick = onBack) + } + AddToPortfolioFooterKind.UserPortfolioAdd -> { + val state = userPortfolioState?.value ?: return + val onClick = onAddFromUserPortfolioClick ?: return + WithLocalHaze { + UserPortfolioAddFooter( + isEnabled = state.isAddEnabled, + onClick = onClick, + ) + } + } + AddToPortfolioFooterKind.None -> Unit + } +} + +@Composable +private fun WithLocalHaze(content: @Composable () -> Unit) { + CompositionLocalProvider(LocalHazeState provides rememberHazeState(), content = content) +} + +@Composable +private fun CancelFooterButton(onClick: () -> Unit) { + SecondaryTangemButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens2.x4) + .padding(bottom = TangemTheme.dimens2.x4), + onClick = onClick, + text = resourceReference(R.string.common_cancel), + size = TangemButtonSize.X12, + shape = TangemButtonShape.Rounded, + ) +} + +@Composable +private fun UserPortfolioAddFooter(isEnabled: Boolean, onClick: () -> Unit) { + TangemRowContainer( + contentPadding = PaddingValues( + horizontal = TangemTheme.dimens2.x6, + vertical = TangemTheme.dimens2.x5, + ), + ) { + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + text = stringResourceSafe(R.string.common_add_token), + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + text = stringResourceSafe(R.string.markets_token_add_subtitle), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + SecondaryTangemButton( + modifier = Modifier + .layoutId(TangemRowLayoutId.TAIL) + .padding(start = TangemTheme.dimens2.x2), + onClick = onClick, + text = resourceReference(R.string.common_add), + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + isEnabled = isEnabled, + ) + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetSwitch.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetSwitch.kt new file mode 100644 index 0000000000..6cb218fbc1 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetSwitch.kt @@ -0,0 +1,34 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM + +@Composable +internal fun AddToPortfolioBottomSheetSwitch( + childStack: State>, + onBack: () -> Unit, + onDismiss: () -> Unit, + userPortfolioState: State? = null, + onAddFromUserPortfolioClick: (() -> Unit)? = null, +) { + if (LocalRedesignEnabled.current) { + AddToPortfolioBottomSheetV2( + childStack = childStack, + onBack = onBack, + onDismiss = onDismiss, + userPortfolioState = userPortfolioState, + onAddFromUserPortfolioClick = onAddFromUserPortfolioClick, + ) + } else { + AddToPortfolioBottomSheet( + childStack = childStack, + onBack = onBack, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetV2.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetV2.kt new file mode 100644 index 0000000000..866fc6456c --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheetV2.kt @@ -0,0 +1,126 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.bottomsheets.* +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec +import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM + +@Composable +internal fun AddToPortfolioBottomSheetV2( + childStack: State>, + onBack: () -> Unit, + onDismiss: () -> Unit, + userPortfolioState: State? = null, + onAddFromUserPortfolioClick: (() -> Unit)? = null, +) { + val stack by childStack + val contentStack = remember { mutableStateOf(stack) } + val isNotEmpty = stack.active.configuration != AddToPortfolioRoutes.Empty + if (isNotEmpty) { + contentStack.value = stack + } + + TangemBottomSheet( + onBack = onBack, + config = TangemBottomSheetConfig( + isShown = isNotEmpty, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + type = TangemBottomSheetType.Modal, + containerColor = TangemTheme.colors2.surface.level2, + title = { + AddToPortfolioBottomSheetTitle( + stack = stack, + onCloseClick = onDismiss, + ) + }, + content = { + AnimatedContent(targetState = contentStack.value, label = "Content Animation") { animatedStack -> + AddToPortfolioRouteContent(animatedStack = animatedStack) + } + }, + footer = { + AnimatedContent( + targetState = contentStack.value.active.configuration, + transitionSpec = { fadeIn() togetherWith fadeOut() }, + label = "Footer Animation", + ) { route -> + AddToPortfolioBottomSheetFooter( + currentRoute = route, + userPortfolioState = userPortfolioState, + onBack = onBack, + onAddFromUserPortfolioClick = onAddFromUserPortfolioClick, + ) + } + }, + ) +} + +@Composable +private fun AddToPortfolioRouteContent(animatedStack: ChildStack) { + val spec = animatedStack.active.configuration.uiSpec() + val baseModifier = if (spec.shouldApplyHorizontalPadding) { + Modifier.padding(horizontal = TangemTheme.dimens2.x4) + } else { + Modifier + } + if (spec.isScrollable) { + val bottomInset = LocalTangemBottomSheetContentBottomInset.current + val scrollBottomReserve = if (bottomInset > 0.dp) bottomInset else TangemTheme.dimens2.x4 + Column(modifier = baseModifier.verticalScroll(rememberScrollState())) { + animatedStack.active.instance.Content(modifier = Modifier) + Spacer(modifier = Modifier.height(scrollBottomReserve)) + } + } else { + animatedStack.active.instance.Content(modifier = baseModifier) + } +} + +@Composable +private fun AddToPortfolioBottomSheetTitle( + stack: ChildStack, + onCloseClick: () -> Unit, +) { + TangemTopBar( + title = stack.active.configuration.uiSpec().title, + type = TangemTopBarType.BottomSheet, + endContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle(onClick = onCloseClick) + .padding(TangemTheme.dimens2.x2), + ) + }, + ) +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt index 189c551c95..02e43ad3eb 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.backStack @@ -83,10 +84,14 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor( @Composable override fun BottomSheet() { - AddToPortfolioBottomSheet( + val userPortfolioState = model.userPortfolioStateController.uiState + .collectAsStateWithLifecycle() + AddToPortfolioBottomSheetSwitch( childStack = childStack.subscribeAsState(), onBack = ::onBack, onDismiss = ::dismiss, + userPortfolioState = userPortfolioState, + onAddFromUserPortfolioClick = model::onContinueFromUserPortfolio, ) } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt index dd8d751766..d266107e75 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolver.kt @@ -23,6 +23,7 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor( selectedWallet: UserWallet?, tokenParams: RawMarketToken, accountToAdd: AvailableToAddAccount? = null, + preferredNetwork: TokenMarketInfo.Network? = null, ): InitialSelection? { if (availableToAddData.availableToAddWallets.isEmpty()) return null val fallbackNetwork = orderedNetworks.firstOrNull() ?: return null @@ -33,13 +34,13 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor( val ownerWallet = walletOrder.firstOrNull { entry -> entry.availableToAddAccounts.values.any { it === accountToAdd } } ?: walletOrder.first() - val network = pickAddableNetwork( + val network = pickNetworkForExplicitAccount( userWallet = ownerWallet.userWallet, account = accountToAdd, orderedNetworks = orderedNetworks, tokenParams = tokenParams, + preferredNetwork = preferredNetwork, ) - ?: fallbackNetwork return InitialSelection(userWallet = ownerWallet.userWallet, account = accountToAdd, network = network) } @@ -81,6 +82,35 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor( ?: walletEntry.availableToAddAccounts.values.firstOrNull() } + private suspend fun pickNetworkForExplicitAccount( + userWallet: UserWallet, + account: AvailableToAddAccount, + orderedNetworks: List, + tokenParams: RawMarketToken, + preferredNetwork: TokenMarketInfo.Network?, + ): TokenMarketInfo.Network { + if (preferredNetwork != null) { + val candidate = account.availableToAddNetworks + .firstOrNull { it.networkId == preferredNetwork.networkId } + if ( + candidate != null && hasDerivationFor( + userWallet = userWallet, + account = account, + network = candidate, + tokenParams = tokenParams, + ) + ) { + return candidate + } + } + return pickAddableNetwork( + userWallet = userWallet, + account = account, + orderedNetworks = orderedNetworks, + tokenParams = tokenParams, + ) ?: orderedNetworks.first() + } + private suspend fun pickAddableNetwork( userWallet: UserWallet, account: AvailableToAddAccount, @@ -92,18 +122,26 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor( } if (availableOrdered.isEmpty()) return null - val derivationIndex = account.account.account.derivationIndex - val withDerivation = availableOrdered.filter { network -> - val currency = getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = tokenParams, - network = network, - accountIndex = derivationIndex, - ) ?: return@filter false - networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false } + val withDerivation = availableOrdered.firstOrNull { network -> + hasDerivationFor(userWallet = userWallet, account = account, network = network, tokenParams = tokenParams) } + return withDerivation ?: availableOrdered.first() + } - return withDerivation.firstOrNull() ?: availableOrdered.first() + private suspend fun hasDerivationFor( + userWallet: UserWallet, + account: AvailableToAddAccount, + network: TokenMarketInfo.Network, + tokenParams: RawMarketToken, + ): Boolean { + val derivationIndex = account.account.account.derivationIndex + val currency = getTokenMarketCryptoCurrency( + userWalletId = userWallet.walletId, + tokenMarketParams = tokenParams, + network = network, + accountIndex = derivationIndex, + ) ?: return false + return networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false } } data class InitialSelection( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index 62226b2e71..0aaee52e02 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -378,23 +378,17 @@ internal class AddToPortfolioModel @Inject constructor( // drop first selected portfolio or any selected before .drop(1) .map { newPortfolio -> - val currentNetwork = selectedNetwork.first().selectedNetwork - val availableToAddNetworks = newPortfolio.account.availableToAddNetworks - val isSelectedNetworkAvailableForNewPortfolio = availableToAddNetworks - .any { it.networkId == currentNetwork.networkId } + val rebuiltSelectedNetwork = selectionResolver.resolve( + availableToAddData = data, + orderedNetworks = orderedNetworks, + selectedWallet = globalSelectedWallet, + tokenParams = tokenParams, + accountToAdd = newPortfolio.account, + preferredNetwork = selectedNetwork.first().selectedNetwork, + )?.toSelectedNetwork() - if (!isSelectedNetworkAvailableForNewPortfolio) { - val selection = selectionResolver.resolve( - availableToAddData = data, - orderedNetworks = orderedNetworks, - selectedWallet = globalSelectedWallet, - tokenParams = tokenParams, - accountToAdd = newPortfolio.account, - ) - val newNetwork = selection?.toSelectedNetwork() - if (newNetwork != null) { - this.selectedNetwork.tryEmit(newNetwork) - } + if (rebuiltSelectedNetwork != null) { + this.selectedNetwork.tryEmit(rebuiltSelectedNetwork) } selectedPortfolio.tryEmit(newPortfolio) navigation.popToFirst() diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt new file mode 100644 index 0000000000..bd256958e8 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt @@ -0,0 +1,57 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.commonfeatures.impl.R + +internal data class AddToPortfolioRouteUiSpec( + val title: TextReference, + val isScrollable: Boolean, + val shouldApplyHorizontalPadding: Boolean, + val footer: AddToPortfolioFooterKind, +) + +internal enum class AddToPortfolioFooterKind { + None, + Cancel, + UserPortfolioAdd, +} + +internal fun AddToPortfolioRoutes.uiSpec(): AddToPortfolioRouteUiSpec = when (this) { + AddToPortfolioRoutes.AddToken -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.common_add_token), + isScrollable = true, + shouldApplyHorizontalPadding = true, + footer = AddToPortfolioFooterKind.None, + ) + is AddToPortfolioRoutes.NetworkSelector -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.common_add_token), + isScrollable = true, + shouldApplyHorizontalPadding = true, + footer = AddToPortfolioFooterKind.Cancel, + ) + AddToPortfolioRoutes.PortfolioSelector -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.common_add_token), + isScrollable = false, + shouldApplyHorizontalPadding = true, + footer = AddToPortfolioFooterKind.Cancel, + ) + AddToPortfolioRoutes.UserPortfolio -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.markets_portfolio_block_title), + isScrollable = false, + shouldApplyHorizontalPadding = false, + footer = AddToPortfolioFooterKind.UserPortfolioAdd, + ) + AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.common_get_token), + isScrollable = true, + shouldApplyHorizontalPadding = true, + footer = AddToPortfolioFooterKind.None, + ) + AddToPortfolioRoutes.Empty -> AddToPortfolioRouteUiSpec( + title = TextReference.EMPTY, + isScrollable = false, + shouldApplyHorizontalPadding = false, + footer = AddToPortfolioFooterKind.None, + ) +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt index d1f169a0ae..b63304be34 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt @@ -150,6 +150,7 @@ internal class TokenActionsUiBuilder @Inject constructor( } else { TangemBadgeIconPosition.End }, + shouldRespectIconTint = cryptoCurrencyData.isAccountMode, ) } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt index b5c7393d49..569d3233b4 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContentV2.kt @@ -44,6 +44,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.* import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal import java.util.UUID @@ -79,13 +80,15 @@ internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = M SpacerH(TangemTheme.dimens2.x2) - SecondaryTangemButton( - modifier = Modifier.fillMaxWidth(), - onClick = state.onLaterClick, - text = resourceReference(R.string.common_later), - size = TangemButtonSize.X12, - shape = TangemButtonShape.Rounded, - ) + CompositionLocalProvider(LocalHazeState provides rememberHazeState()) { + SecondaryTangemButton( + modifier = Modifier.fillMaxWidth(), + onClick = state.onLaterClick, + text = resourceReference(R.string.common_later), + size = TangemButtonSize.X12, + shape = TangemButtonShape.Rounded, + ) + } } } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt index d09e0cfcaf..42d36a8582 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/userportfolio/DefaultUserPortfolioComponent.kt @@ -3,12 +3,10 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.ui.markets.tokenselector.StickyFooter import com.tangem.common.ui.markets.tokenselector.TokenSelectorEmbeddedContent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.commonfeatures.impl.R +import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -20,7 +18,6 @@ internal class DefaultUserPortfolioComponent @AssistedInject constructor( ) : UserPortfolioComponent, AppComponentContext by context { private val model: UserPortfolioModel = getOrCreateModel(params) - private val onContinueClick: () -> Unit = params.callbacks::onContinueFromUserPortfolio @Composable override fun Content(modifier: Modifier) { @@ -29,11 +26,7 @@ internal class DefaultUserPortfolioComponent @AssistedInject constructor( TokenSelectorEmbeddedContent( content = state.content, modifier = modifier, - stickyFooter = StickyFooter( - buttonText = resourceReference(R.string.common_add), - isEnabled = state.isAddEnabled, - onClick = onContinueClick, - ), + scrollBottomInset = LocalTangemBottomSheetContentBottomInset.current, ) } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorComponent.kt index 1d3df6157b..3346d4017d 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorComponent.kt @@ -1,11 +1,13 @@ package com.tangem.features.commonfeatures.impl.portfolioselector +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent @@ -45,15 +47,18 @@ internal class DefaultPortfolioSelectorComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val listBottomPadding = PaddingValues(bottom = LocalTangemBottomSheetContentBottomInset.current) if (LocalRedesignEnabled.current) { PortfolioSelectorContentV2( state = state, modifier = modifier, + contentPadding = listBottomPadding, ) } else { PortfolioSelectorContent( state = state, modifier = modifier, + contentPadding = listBottomPadding, ) } } diff --git a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt index 3e6307f8ed..0e42020f57 100644 --- a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt +++ b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioInitialSelectionResolverTest.kt @@ -508,6 +508,109 @@ class AddToPortfolioInitialSelectionResolverTest { Truth.assertThat(result?.network).isEqualTo(BITCOIN) } + @Test + fun `GIVEN accountToAdd and preferredNetwork is on account and addable WHEN resolve THEN return preferred`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right() + every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(BITCOIN, ETHEREUM), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + preferredNetwork = ETHEREUM, + ) + + Truth.assertThat(result?.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN accountToAdd and preferred is not on account WHEN resolve THEN use pickAddableNetwork`() = runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right() + every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM, BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + preferredNetwork = POLYGON, + ) + + Truth.assertThat(result?.network).isEqualTo(ETHEREUM) + } + + @Test + fun `GIVEN accountToAdd and preferred on account has no derivation WHEN resolve THEN fall back to pickAddableNetwork`() = + runTest { + val walletId = UserWalletId(WALLET_ID_A) + val userWallet = userWallet(walletId) + val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN)) + + val ethereumCurrency = cryptoCurrency() + val bitcoinCurrency = cryptoCurrency() + coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency + coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency + every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns false.right() + every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right() + + val data = availableData( + wallets = mapOf( + walletId to walletEntry( + userWallet = userWallet, + accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()), + ), + ), + ) + + val result = resolver.resolve( + availableToAddData = data, + orderedNetworks = listOf(ETHEREUM, BITCOIN), + selectedWallet = userWallet, + tokenParams = tokenParams, + accountToAdd = explicitAccount, + preferredNetwork = ETHEREUM, + ) + + Truth.assertThat(result?.network).isEqualTo(BITCOIN) + } + @Test fun `GIVEN get token market crypto currency returns null WHEN resolve THEN fall back to first ordered available network`() = runTest { val walletId = UserWalletId(WALLET_ID_A) @@ -587,5 +690,12 @@ class AddToPortfolioInitialSelectionResolverTest { contractAddress = null, decimalCount = 8, ) + + val POLYGON = TokenMarketInfo.Network( + networkId = "polygon", + isExchangeable = true, + contractAddress = null, + decimalCount = 18, + ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt index e372da6cda..7ca136dcf8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt @@ -10,7 +10,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BottomSheetDefaults @@ -22,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -31,6 +31,8 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -56,6 +58,7 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi } AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), visible = isVisible, enter = fadeIn(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)), exit = fadeOut(animationSpec = tween(durationMillis = 300)), @@ -91,62 +94,55 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi @Composable private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) { - FloatingCard(modifier = modifier) { // TODO will be handle in [REDACTED_TASK_KEY] - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = Modifier - .weight(1f) - .clickable(onClick = state.onRowClick), - verticalAlignment = Alignment.CenterVertically, - ) { - CurrencyIcon( - modifier = Modifier.padding(end = TangemTheme.dimens2.x3), - state = state.tokenIcon, - ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = state.tokenName, - style = TangemTheme.typography2.bodyMedium16, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - ) - Text( - text = stringResourceSafe(R.string.markets_portfolio_block_subtitle), - style = TangemTheme.typography2.captionMedium12, - color = TangemTheme.colors2.text.neutral.secondary, - maxLines = 1, - ) - } - Column(horizontalAlignment = Alignment.End) { - Text( - text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = TangemTheme.colors2.text.neutral.primary, - style = TangemTheme.typography2.bodyMedium16, - ) - Text( - text = state.tokenSymbol, - style = TangemTheme.typography2.captionMedium12, - color = TangemTheme.colors2.text.neutral.secondary, - maxLines = 1, - ) - } - } + FloatingCard(modifier = modifier) { + TangemRowContainer(modifier = Modifier.clickableSingle(onClick = state.onRowClick)) { + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = TangemTheme.colors2.text.neutral.primary, + style = TangemTheme.typography2.bodyMedium16, + ) + + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + text = stringResourceSafe(R.string.markets_portfolio_block_subtitle), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + TangemButton( - modifier = Modifier.padding(start = TangemTheme.dimens2.x3), + modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP), + buttonUM = TangemButtonUM( + text = resourceReference(R.string.common_add_funds), + type = TangemButtonType.Secondary, + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_arrow_down_20, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + iconPosition = TangemButtonIconPosition.Start, + shape = TangemButtonShape.Rounded, + size = TangemButtonSize.X9, + onClick = state.onAddFundsClick, + ), + ) + + TangemButton( + modifier = Modifier + .layoutId(TangemRowLayoutId.TAIL) + .padding(start = TangemTheme.dimens2.x3), buttonUM = TangemButtonUM( type = TangemButtonType.Secondary, tangemIconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_chevron_24, + iconRes = R.drawable.ic_arrow_expand_24, tintReference = { TangemTheme.colors2.graphic.neutral.primary }, ), shape = TangemButtonShape.Rounded, size = TangemButtonSize.X9, - onClick = state.onAddFundsClick, + onClick = state.onRowClick, ), ) } @@ -156,7 +152,12 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M @Composable private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) { FloatingCard(modifier = modifier) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier + .padding(TangemTheme.dimens2.x3) + .clickableSingle(onClick = state.onAddClick), + verticalAlignment = Alignment.CenterVertically, + ) { CurrencyIcon(state.tokenIcon) SpacerW(TangemTheme.dimens2.x3) @@ -204,8 +205,7 @@ private fun FloatingCard(modifier: Modifier = Modifier, content: @Composable () .background( color = TangemTheme.colors2.surface.level3, shape = RoundedCornerShape(size = TangemTheme.dimens2.x5), - ) - .padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3), + ), ) { content() } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt index eaf7f1665d..d0bf1b73b9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt @@ -17,6 +17,7 @@ import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.res.LocalHazeState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled @@ -130,6 +131,12 @@ private fun EntryContentV2( child.instance.Content( modifier = Modifier .fillMaxSize() + .conditionalCompose( + condition = !isOpenedInBottomSheet, + modifier = { + padding(top = topBarHeight) + }, + ) .hazeSourceTangem(zIndex = 0f, state = hazeState), contentPadding = PaddingValues(top = topBarHeight), bottomSheetState = bottomSheetState, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 79330be97f..3377a8eb08 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -10,13 +10,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -27,11 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons @@ -171,7 +161,7 @@ private fun Content( Modifier .onSizeChanged { size -> bottomSpacing = if (size.height > 0) { - with(density) { size.height.toDp() + 16.dp } + with(density) { size.height.toDp() } } else { 0.dp }