Updated on 2026-08-14
This commit is contained in:
parent
8511623421
commit
7095ceec0f
21 changed files with 717 additions and 183 deletions
|
|
@ -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<TokenSelectorContentUM>(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <reified T : TangemBottomSheetConfigContent> 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<Dp?>(null) }
|
||||
|
||||
val bsContent: @Composable ColumnScope.() -> Unit = {
|
||||
val maxHeight = when (type) {
|
||||
|
|
@ -212,17 +224,19 @@ inline fun <reified T : TangemBottomSheetConfigContent> 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 <reified T : TangemBottomSheetConfigContent> 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 <reified T : TangemBottomSheetConfigContent> 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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
9
core/ui/src/main/res/drawable/ic_arrow_down_20.xml
Normal file
9
core/ui/src/main/res/drawable/ic_arrow_down_20.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,2.5C10.46,2.5 10.833,2.873 10.833,3.333V15.071L16.077,9.827C16.402,9.502 16.93,9.502 17.256,9.827C17.581,10.153 17.581,10.681 17.256,11.006L10.589,17.672L10,18.262L2.744,11.006C2.419,10.68 2.419,10.153 2.744,9.827C3.069,9.502 3.596,9.502 3.922,9.827L9.166,15.071V3.333C9.166,2.873 9.54,2.5 10,2.5Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
|
@ -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<UserPortfolioUM?>?,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
|
||||
onBack: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
userPortfolioState: State<UserPortfolioUM?>? = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
|
||||
onBack: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
userPortfolioState: State<UserPortfolioUM?>? = 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<TangemBottomSheetConfigContent.Empty>(
|
||||
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<AddToPortfolioRoutes, ComposableContentComponent>) {
|
||||
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<AddToPortfolioRoutes, ComposableContentComponent>,
|
||||
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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TokenMarketInfo.Network>,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
|
|||
} else {
|
||||
TangemBadgeIconPosition.End
|
||||
},
|
||||
shouldRespectIconTint = cryptoCurrencyData.isAccountMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue