Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-11 14:56:41 +02:00
parent 646349ff69
commit f467cbacd0
14 changed files with 786 additions and 310 deletions

View file

@ -1,305 +1,23 @@
package com.tangem.core.ui.components.bottomsheets.message
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM.Button.IconOrder
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.WarningBottomSheetTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import com.tangem.core.ui.res.LocalRedesignEnabled
@Composable
fun MessageBottomSheet(state: MessageBottomSheetUM, onDismissRequest: () -> Unit) {
val stateWithOnDismiss = remember(state) {
state.copy(
onDismissRequest = {
state.onDismissRequest.invoke()
onDismissRequest()
},
)
if (LocalRedesignEnabled.current) {
MessageBottomSheetV2(state, onDismissRequest)
} else {
MessageBottomSheetV1(state, onDismissRequest)
}
val config = TangemBottomSheetConfig(
isShown = true,
content = stateWithOnDismiss,
onDismissRequest = stateWithOnDismiss.onDismissRequest,
)
TangemModalBottomSheet(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = stateWithOnDismiss.onDismissRequest,
)
},
content = { content: MessageBottomSheetUM -> MessageBottomSheetContent(content) },
)
}
@Composable
fun MessageBottomSheetContent(state: MessageBottomSheetUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.elements.fastForEach { element ->
when (element) {
is MessageBottomSheetUM.InfoBlock -> {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
state = element,
)
}
else -> Unit
}
}
ButtonsContainer(
modifier = Modifier.fillMaxWidth(),
closeScope = state.closeScope,
buttons = state.elements.filterIsInstance<MessageBottomSheetUM.Button>().toPersistentList(),
)
}
}
@Composable
private fun ContentContainer(state: MessageBottomSheetUM.InfoBlock, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
BottomSheetIconContainer(state.icon, state.iconImage)
state.title?.let { title ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24)
.testTag(WarningBottomSheetTestTags.TITLE),
text = title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
state.body?.let { body ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8)
.testTag(WarningBottomSheetTestTags.MESSAGE),
text = body.resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
state.chip?.let { chip ->
BottomSheetChip(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing16),
chip = chip,
)
}
}
}
@Composable
private fun BottomSheetIconContainer(
icon: MessageBottomSheetUM.Icon?,
iconImage: MessageBottomSheetUM.IconImage?,
modifier: Modifier = Modifier,
) {
if (icon != null) {
BottomSheetIcon(icon, modifier)
} else if (iconImage != null) {
Image(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape),
painter = painterResource(id = iconImage.res),
contentDescription = null,
)
}
}
@Composable
private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) {
val tint = when (icon.type) {
MessageBottomSheetUM.Icon.Type.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Icon.Type.Accent -> TangemTheme.colors.icon.accent
MessageBottomSheetUM.Icon.Type.Informative -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.Type.Attention -> TangemTheme.colors.icon.attention
MessageBottomSheetUM.Icon.Type.Warning -> TangemTheme.colors.icon.warning
}
val backgroundColor = when (icon.backgroundType) {
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint
MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors.icon.accent
MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors.icon.attention
MessageBottomSheetUM.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
}
HighlightedIcon(
modifier = modifier,
icon = icon.res,
iconTint = tint,
backgroundColor = backgroundColor,
)
}
@Composable
private fun BottomSheetChip(chip: MessageBottomSheetUM.Chip, modifier: Modifier = Modifier) {
val color = when (chip.type) {
MessageBottomSheetUM.Chip.Type.Unspecified -> TangemTheme.colors.text.primary1
MessageBottomSheetUM.Chip.Type.Warning -> TangemTheme.colors.text.warning
}
Text(
modifier = modifier
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = color.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = chip.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = color,
)
}
@Suppress("LongMethod")
@Composable
private fun ButtonsContainer(
buttons: ImmutableList<MessageBottomSheetUM.Button>,
closeScope: MessageBottomSheetUM.CloseScope,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
buttons.fastForEach { button ->
val icon = button.icon?.let { iconResId ->
when (button.iconOrder) {
IconOrder.Start -> TangemButtonIconPosition.Start(iconResId)
IconOrder.End -> TangemButtonIconPosition.End(iconResId)
}
} ?: TangemButtonIconPosition.None
TangemButton(
modifier = Modifier
.fillMaxWidth()
.testTag(
if (button.isPrimary) {
WarningBottomSheetTestTags.BUTTON_PRIMARY
} else {
WarningBottomSheetTestTags.BUTTON_SECONDARY
},
),
text = button.text?.resolveReference().orEmpty(),
icon = icon,
onClick = { button.onClick?.invoke(closeScope) },
colors = if (button.isPrimary) {
TangemButtonsDefaults.primaryButtonColors
} else {
TangemButtonsDefaults.secondaryButtonColors
},
enabled = true,
showProgress = false,
textStyle = TangemTheme.typography.subtitle1,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUM.Icon.Type.Attention
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.SameAsTint
}
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview2() {
TangemThemePreview {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
iconImage = MessageBottomSheetUM.IconImage(R.drawable.img_visa_notification)
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
if (LocalRedesignEnabled.current) {
MessageBottomSheetContentV2(state, modifier)
} else {
MessageBottomSheetContentV1(state, modifier)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.bottomsheets.message
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.vector.ImageVector
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -41,6 +42,21 @@ data class MessageBottomSheetUM(
}
}
@Immutable
data class Vector(
val imageVector: ImageVector,
var type: Type = Type.Unspecified,
var backgroundType: BackgroundType = BackgroundType.Unspecified,
) : Element {
enum class Type {
Unspecified, Accent, Informative, Attention, Warning,
}
enum class BackgroundType {
Unspecified, SameAsTint, Accent, Informative, Attention, Warning,
}
}
@Immutable
data class IconImage(@DrawableRes internal var res: Int) : Element
@ -58,6 +74,7 @@ data class MessageBottomSheetUM(
data class InfoBlock(
internal var icon: Icon? = null,
internal var iconImage: IconImage? = null,
internal var vector: Vector? = null,
internal var chip: Chip? = null,
var title: TextReference? = null,
var body: TextReference? = null,
@ -111,6 +128,12 @@ fun MessageBottomSheetUM.InfoBlock.icon(@DrawableRes res: Int, init: MessageBott
icon = MessageBottomSheetUM.Icon(res).apply(init)
}
@Suppress("NestedScopeFunctions")
fun MessageBottomSheetUM.InfoBlock.vector(imageVector: ImageVector, init: MessageBottomSheetUM.Vector.() -> Unit = {}) =
apply {
vector = MessageBottomSheetUM.Vector(imageVector).apply(init)
}
fun MessageBottomSheetUM.InfoBlock.iconImage(@DrawableRes res: Int) = apply {
iconImage = MessageBottomSheetUM.IconImage(res)
}

View file

@ -0,0 +1,305 @@
package com.tangem.core.ui.components.bottomsheets.message
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM.Button.IconOrder
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.WarningBottomSheetTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
fun MessageBottomSheetV1(state: MessageBottomSheetUM, onDismissRequest: () -> Unit) {
val stateWithOnDismiss = remember(state) {
state.copy(
onDismissRequest = {
state.onDismissRequest.invoke()
onDismissRequest()
},
)
}
val config = TangemBottomSheetConfig(
isShown = true,
content = stateWithOnDismiss,
onDismissRequest = stateWithOnDismiss.onDismissRequest,
)
TangemModalBottomSheet(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = stateWithOnDismiss.onDismissRequest,
)
},
content = { content: MessageBottomSheetUM -> MessageBottomSheetContent(content) },
)
}
@Composable
fun MessageBottomSheetContentV1(state: MessageBottomSheetUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.elements.fastForEach { element ->
when (element) {
is MessageBottomSheetUM.InfoBlock -> {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
state = element,
)
}
else -> Unit
}
}
ButtonsContainer(
modifier = Modifier.fillMaxWidth(),
closeScope = state.closeScope,
buttons = state.elements.filterIsInstance<MessageBottomSheetUM.Button>().toPersistentList(),
)
}
}
@Composable
private fun ContentContainer(state: MessageBottomSheetUM.InfoBlock, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
BottomSheetIconContainer(state.icon, state.iconImage)
state.title?.let { title ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24)
.testTag(WarningBottomSheetTestTags.TITLE),
text = title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
state.body?.let { body ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8)
.testTag(WarningBottomSheetTestTags.MESSAGE),
text = body.resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
state.chip?.let { chip ->
BottomSheetChip(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing16),
chip = chip,
)
}
}
}
@Composable
private fun BottomSheetIconContainer(
icon: MessageBottomSheetUM.Icon?,
iconImage: MessageBottomSheetUM.IconImage?,
modifier: Modifier = Modifier,
) {
if (icon != null) {
BottomSheetIcon(icon, modifier)
} else if (iconImage != null) {
Image(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape),
painter = painterResource(id = iconImage.res),
contentDescription = null,
)
}
}
@Composable
private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) {
val tint = when (icon.type) {
MessageBottomSheetUM.Icon.Type.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Icon.Type.Accent -> TangemTheme.colors.icon.accent
MessageBottomSheetUM.Icon.Type.Informative -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.Type.Attention -> TangemTheme.colors.icon.attention
MessageBottomSheetUM.Icon.Type.Warning -> TangemTheme.colors.icon.warning
}
val backgroundColor = when (icon.backgroundType) {
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint
MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors.icon.accent
MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors.icon.informative
MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors.icon.attention
MessageBottomSheetUM.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
}
HighlightedIcon(
modifier = modifier,
icon = icon.res,
iconTint = tint,
backgroundColor = backgroundColor,
)
}
@Composable
private fun BottomSheetChip(chip: MessageBottomSheetUM.Chip, modifier: Modifier = Modifier) {
val color = when (chip.type) {
MessageBottomSheetUM.Chip.Type.Unspecified -> TangemTheme.colors.text.primary1
MessageBottomSheetUM.Chip.Type.Warning -> TangemTheme.colors.text.warning
}
Text(
modifier = modifier
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = color.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = chip.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = color,
)
}
@Suppress("LongMethod")
@Composable
private fun ButtonsContainer(
buttons: ImmutableList<MessageBottomSheetUM.Button>,
closeScope: MessageBottomSheetUM.CloseScope,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
buttons.fastForEach { button ->
val icon = button.icon?.let { iconResId ->
when (button.iconOrder) {
IconOrder.Start -> TangemButtonIconPosition.Start(iconResId)
IconOrder.End -> TangemButtonIconPosition.End(iconResId)
}
} ?: TangemButtonIconPosition.None
TangemButton(
modifier = Modifier
.fillMaxWidth()
.testTag(
if (button.isPrimary) {
WarningBottomSheetTestTags.BUTTON_PRIMARY
} else {
WarningBottomSheetTestTags.BUTTON_SECONDARY
},
),
text = button.text?.resolveReference().orEmpty(),
icon = icon,
onClick = { button.onClick?.invoke(closeScope) },
colors = if (button.isPrimary) {
TangemButtonsDefaults.primaryButtonColors
} else {
TangemButtonsDefaults.secondaryButtonColors
},
enabled = true,
showProgress = false,
textStyle = TangemTheme.typography.subtitle1,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUM.Icon.Type.Attention
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.SameAsTint
}
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview2() {
TangemThemePreview {
MessageBottomSheetV1(
messageBottomSheetUM {
infoBlock {
iconImage = MessageBottomSheetUM.IconImage(R.drawable.img_visa_notification)
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}

View file

@ -0,0 +1,369 @@
package com.tangem.core.ui.components.bottomsheets.message
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.WarningBottomSheetTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
fun MessageBottomSheetV2(state: MessageBottomSheetUM, onDismissRequest: () -> Unit) {
val stateWithOnDismiss = remember(state) {
state.copy(
onDismissRequest = {
state.onDismissRequest.invoke()
onDismissRequest()
},
)
}
val config = TangemBottomSheetConfig(
isShown = true,
content = stateWithOnDismiss,
onDismissRequest = stateWithOnDismiss.onDismissRequest,
)
TangemBottomSheet(
config = config,
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = stateWithOnDismiss.onDismissRequest,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
},
content = { content: MessageBottomSheetUM ->
MessageBottomSheetContent(
modifier = Modifier.padding(vertical = TangemTheme.dimens2.x4),
state = content,
)
},
)
}
@Composable
fun MessageBottomSheetContentV2(state: MessageBottomSheetUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.elements.fastForEach { element ->
when (element) {
is MessageBottomSheetUM.InfoBlock -> {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
state = element,
)
}
else -> Unit
}
}
ButtonsContainer(
modifier = Modifier.fillMaxWidth(),
closeScope = state.closeScope,
buttons = state.elements.filterIsInstance<MessageBottomSheetUM.Button>().toPersistentList(),
)
}
}
@Composable
private fun ContentContainer(state: MessageBottomSheetUM.InfoBlock, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
BottomSheetIconContainer(state.icon, state.iconImage, state.vector)
state.title?.let { title ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x8)
.testTag(WarningBottomSheetTestTags.TITLE),
text = title.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
)
}
state.body?.let { body ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x2)
.testTag(WarningBottomSheetTestTags.MESSAGE),
text = body.resolveAnnotatedReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
)
}
state.chip?.let { chip ->
BottomSheetChip(
modifier = Modifier.padding(top = TangemTheme.dimens2.x4),
chip = chip,
)
}
}
}
@Composable
private fun BottomSheetIconContainer(
icon: MessageBottomSheetUM.Icon?,
iconImage: MessageBottomSheetUM.IconImage?,
vector: MessageBottomSheetUM.Vector?,
modifier: Modifier = Modifier,
) {
if (icon != null) {
BottomSheetIcon(icon, modifier)
} else if (iconImage != null) {
Image(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape),
painter = painterResource(id = iconImage.res),
contentDescription = null,
)
} else if (vector != null) {
BottomSheetVector(vector, modifier)
}
}
@Composable
private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) {
val tint = when (icon.type) {
MessageBottomSheetUM.Icon.Type.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Icon.Type.Accent -> TangemTheme.colors3.icon.status.info
MessageBottomSheetUM.Icon.Type.Informative -> TangemTheme.colors3.icon.status.info
MessageBottomSheetUM.Icon.Type.Attention -> TangemTheme.colors3.icon.status.warning
MessageBottomSheetUM.Icon.Type.Warning -> TangemTheme.colors3.icon.status.error
}
val backgroundColor = when (icon.backgroundType) {
MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint
MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors3.bg.status.warningSubtle
MessageBottomSheetUM.Icon.BackgroundType.Warning -> TangemTheme.colors3.bg.status.errorSubtle
}
Box(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape)
.background(backgroundColor)
.testTag(WarningBottomSheetTestTags.ICON),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(28.dp),
painter = painterResource(icon.res),
contentDescription = null,
tint = tint,
)
},
)
}
@Composable
private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Modifier = Modifier) {
val tint = when (vector.type) {
MessageBottomSheetUM.Vector.Type.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Vector.Type.Accent -> TangemTheme.colors3.icon.status.info
MessageBottomSheetUM.Vector.Type.Informative -> TangemTheme.colors3.icon.status.info
MessageBottomSheetUM.Vector.Type.Attention -> TangemTheme.colors3.icon.status.warning
MessageBottomSheetUM.Vector.Type.Warning -> TangemTheme.colors3.icon.status.error
}
val backgroundColor = when (vector.backgroundType) {
MessageBottomSheetUM.Vector.BackgroundType.Unspecified -> Color.Unspecified
MessageBottomSheetUM.Vector.BackgroundType.SameAsTint -> tint
MessageBottomSheetUM.Vector.BackgroundType.Accent -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Vector.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Vector.BackgroundType.Attention -> TangemTheme.colors3.bg.status.warningSubtle
MessageBottomSheetUM.Vector.BackgroundType.Warning -> TangemTheme.colors3.bg.status.errorSubtle
}
Box(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape)
.background(backgroundColor)
.testTag(WarningBottomSheetTestTags.ICON),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(28.dp),
imageVector = vector.imageVector,
contentDescription = null,
tint = tint,
)
},
)
}
@Composable
private fun BottomSheetChip(chip: MessageBottomSheetUM.Chip, modifier: Modifier = Modifier) {
val color = when (chip.type) {
MessageBottomSheetUM.Chip.Type.Unspecified -> TangemTheme.colors.text.primary1
MessageBottomSheetUM.Chip.Type.Warning -> TangemTheme.colors.text.warning
}
Text(
modifier = modifier
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = color.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = chip.text.resolveReference(),
style = TangemTheme.typography.caption1,
color = color,
)
}
@Suppress("LongMethod")
@Composable
private fun ButtonsContainer(
buttons: ImmutableList<MessageBottomSheetUM.Button>,
closeScope: MessageBottomSheetUM.CloseScope,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(all = TangemTheme.dimens2.x4),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
buttons.fastForEach { button ->
TangemButton(
modifier = Modifier
.fillMaxWidth()
.testTag(
if (button.isPrimary) {
WarningBottomSheetTestTags.BUTTON_PRIMARY
} else {
WarningBottomSheetTestTags.BUTTON_SECONDARY
},
),
onClick = { button.onClick?.invoke(closeScope) },
text = button.text ?: TextReference.EMPTY,
iconStart = if (button.iconOrder == MessageBottomSheetUM.Button.IconOrder.Start) {
button.icon?.let(TangemIconUM::Icon)
} else {
null
},
iconEnd = if (button.iconOrder == MessageBottomSheetUM.Button.IconOrder.End) {
button.icon?.let(TangemIconUM::Icon)
} else {
null
},
variant = if (button.isPrimary) {
TangemButton.Variant.Primary
} else {
TangemButton.Variant.Secondary
},
size = TangemButton.Size.X12,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreviewRedesign {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUM.Icon.Type.Attention
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.SameAsTint
}
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview2() {
TangemThemePreviewRedesign {
MessageBottomSheet(
messageBottomSheetUM {
infoBlock {
iconImage = MessageBottomSheetUM.IconImage(R.drawable.img_visa_notification)
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}

View file

@ -88,7 +88,7 @@ private fun lightThemeColors2(): TangemColors2 {
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark3,
tertiary = TangemColorPalette.Dark1,
quaternary = TangemColorPalette.Light4,
primaryInvertedConstant = TangemColorPalette.White,
tertiaryConstant = TangemColorPalette.Dark1,

View file

@ -29,6 +29,7 @@ internal class TangemPayDetailsStateFactory(
isTangemPayDeactivated: Boolean,
cardNumberEnd: String,
isReissuing: Boolean,
isFrozen: Boolean,
): TangemPayDetailsUM {
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
@ -49,6 +50,7 @@ internal class TangemPayDetailsStateFactory(
lastDigits = cardNumberEnd,
onClick = {},
isReissuing = isReissuing,
isFrozen = isFrozen,
),
),
onAddCardClick = intents::onAddCardClick,

View file

@ -92,6 +92,7 @@ internal sealed class TangemPayDetailsBalanceBlockState {
val lastDigits: String,
val onClick: () -> Unit,
val isReissuing: Boolean,
val isFrozen: Boolean,
)
}

View file

@ -293,9 +293,15 @@ internal class TangemPayCardPageModel @Inject constructor(
if (frozenStateJobHolder.isActive) return
val message = if (isFrozen) {
TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard)
TangemPayMessagesFactory.createUnfreezeCardMessage(
onUnfreezeClicked = ::unfreezeCard,
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
)
} else {
TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard)
TangemPayMessagesFactory.createFreezeCardMessage(
onFreezeClicked = ::freezeCard,
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
)
}
uiMessageSender.send(message)
}

View file

@ -106,6 +106,7 @@ internal class TangemPayDetailsModel @Inject constructor(
isTangemPayDeactivated = isTangemPayDeactivated,
cardNumberEnd = initialCard?.lastDigits.orEmpty(),
isReissuing = initialCard == null || initialCard.state != TangemPayCardState.Active,
isFrozen = initialCard?.frozenState == TangemPayCardFrozenState.Frozen,
),
)

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
@ -17,6 +18,7 @@ internal class TangemPayCardDataTransformer(
lastDigits = card.lastDigits,
onClick = onCardClick,
isReissuing = card.state != TangemPayCardState.Active,
isFrozen = card.frozenState == TangemPayCardFrozenState.Frozen,
)
val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy(
cards = persistentListOf(updatedCard),

View file

@ -460,8 +460,18 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
isBalanceFlickering = false,
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = persistentListOf(
TangemPayDetailsBalanceBlockState.Card(lastDigits = "1234", onClick = {}, isReissuing = false),
TangemPayDetailsBalanceBlockState.Card(lastDigits = "3456", onClick = {}, isReissuing = false),
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "1234",
onClick = {},
isReissuing = false,
isFrozen = false,
),
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "3456",
onClick = {},
isReissuing = false,
isFrozen = false,
),
),
onAddCardClick = {},
),
@ -490,6 +500,7 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
lastDigits = "1234",
onClick = {},
isReissuing = true,
isFrozen = false,
),
),
onAddCardClick = {},

View file

@ -340,6 +340,7 @@ private fun CardsBlock(
isReissuing = item.isReissuing,
lastDigits = item.lastDigits,
onClick = item.onClick,
isFrozen = item.isFrozen,
)
SpacerW(TangemTheme.dimens2.x2)
}

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_clock_12
import com.tangem.core.ui.res.generated.icons.ic_cloud_12_filled
import com.tangem.core.ui.res.generated.icons.ic_snowflake_16
import com.tangem.core.ui.test.TangemPayTestTags
private const val DEFAULT_CARD_BG = 0xFF1C1F29
@ -42,6 +43,7 @@ internal fun TangemPayCardView(
isReissuing: Boolean,
lastDigits: String,
onClick: () -> Unit,
isFrozen: Boolean,
modifier: Modifier = Modifier,
) {
CardBackground(
@ -64,10 +66,10 @@ internal fun TangemPayCardView(
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
imageVector = if (isReissuing) {
Icons.ic_clock_12
} else {
Icons.ic_cloud_12_filled
imageVector = when {
isFrozen -> Icons.ic_snowflake_16
isReissuing -> Icons.ic_clock_12
else -> Icons.ic_cloud_12_filled
},
tint = TangemTheme.colors3.icon.staticDark,
contentDescription = null,
@ -191,11 +193,13 @@ private fun CardBackgroundPreview() {
content = {},
)
SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234")
TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isFrozen = false)
SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = true, onClick = {}, lastDigits = "")
TangemPayCardView(isReissuing = true, onClick = {}, lastDigits = "", isFrozen = false)
SpacerH(TangemTheme.dimens2.x4)
TangemPayAddCardView(onClick = {})
SpacerH(TangemTheme.dimens2.x4)
TangemPayCardView(isReissuing = false, onClick = {}, lastDigits = "1234", isFrozen = true)
}
}
}

View file

@ -6,6 +6,9 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_snowflake_20
import com.tangem.core.ui.res.generated.icons.ic_sun_20
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
internal object TangemPayMessagesFactory {
@ -55,16 +58,31 @@ internal object TangemPayMessagesFactory {
}
}
fun createFreezeCardMessage(onFreezeClicked: () -> Unit): BottomSheetMessage {
fun createFreezeCardMessage(isRedesignEnabled: Boolean, onFreezeClicked: () -> Unit): BottomSheetMessage {
return bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUM.Icon.Type.Accent
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Accent
if (isRedesignEnabled) {
vector(Icons.ic_snowflake_20) {
type = MessageBottomSheetUM.Vector.Type.Informative
backgroundType = MessageBottomSheetUM.Vector.BackgroundType.Informative
}
} else {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUM.Icon.Type.Accent
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Accent
}
}
title = TextReference.Res(R.string.tangem_pay_freeze_card_alert_title)
body = TextReference.Res(R.string.tangem_pay_freeze_card_alert_body)
}
if (isRedesignEnabled) {
secondaryButton {
text = resourceReference(R.string.common_cancel)
onClick {
closeBs()
}
}
}
primaryButton {
text = resourceReference(R.string.tangem_pay_freeze_card_freeze)
onClick {
@ -75,16 +93,31 @@ internal object TangemPayMessagesFactory {
}
}
fun createUnfreezeCardMessage(onUnfreezeClicked: () -> Unit): BottomSheetMessage {
fun createUnfreezeCardMessage(isRedesignEnabled: Boolean, onUnfreezeClicked: () -> Unit): BottomSheetMessage {
return bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUM.Icon.Type.Accent
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Accent
if (isRedesignEnabled) {
vector(Icons.ic_sun_20) {
type = MessageBottomSheetUM.Vector.Type.Attention
backgroundType = MessageBottomSheetUM.Vector.BackgroundType.Attention
}
} else {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUM.Icon.Type.Accent
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Accent
}
}
title = TextReference.Res(R.string.tangem_pay_unfreeze_card_alert_title)
body = TextReference.Res(R.string.tangem_pay_unfreeze_card_alert_body)
}
if (isRedesignEnabled) {
secondaryButton {
text = resourceReference(R.string.common_cancel)
onClick {
closeBs()
}
}
}
primaryButton {
text = resourceReference(R.string.tangempay_card_details_unfreeze_card)
onClick {