Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-26 12:36:27 +03:00
parent cb50b7cf07
commit 0c2d933601
4 changed files with 394 additions and 0 deletions

View file

@ -0,0 +1,134 @@
package com.tangem.core.ui.components.bottomsheets.message
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Immutable
data class MessageBottomSheetUMV2(
var elements: ImmutableList<Element> = persistentListOf(),
var onDismissRequest: () -> Unit = {},
) : TangemBottomSheetConfigContent {
@Immutable
inner class CloseScope {
fun closeBs() {
onDismissRequest()
}
}
val closeScope = CloseScope()
@Immutable
sealed interface Element
@Immutable
data class Icon(
@DrawableRes internal var res: Int,
var type: Type = Type.Unspecified,
var backgroundType: BackgroundType = BackgroundType.Unspecified,
) : Element {
enum class Type {
Unspecified, Accent, Informative, Attention, Warning,
}
enum class BackgroundType {
Unspecified, SameAsTint
}
}
@Immutable
data class Chip(
internal var text: TextReference,
var type: Type = Type.Unspecified,
) : Element {
enum class Type {
Unspecified, Warning
}
}
@Immutable
data class InfoBlock(
internal var icon: Icon? = null,
internal var chip: Chip? = null,
var title: TextReference? = null,
var body: TextReference? = null,
) : Element
@Immutable
data class Button(
internal var isPrimary: Boolean = false,
var text: TextReference? = null,
@DrawableRes internal var iconInternal: Int? = null,
internal var iconOrder: IconOrder = IconOrder.Start,
var onClick: (CloseScope.() -> Unit)? = null,
) : Element {
var icon: Int? = iconInternal
set(value) {
iconOrder = if (text == null) {
IconOrder.Start
} else {
IconOrder.End
}
field = value
}
enum class IconOrder {
Start, End
}
}
}
@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class MessageBottomSheetV2Dsl
// region: DSL
internal fun messageBottomSheetUM(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.() -> Unit) =
MessageBottomSheetUMV2().apply(init)
fun MessageBottomSheetUMV2.onDismiss(block: () -> Unit) = MessageBottomSheetUMV2().apply { onDismissRequest = block }
fun MessageBottomSheetUMV2.infoBlock(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.InfoBlock.() -> Unit) =
apply {
val element = MessageBottomSheetUMV2.InfoBlock().apply(init)
elements = (elements + element).toPersistentList()
}
fun MessageBottomSheetUMV2.InfoBlock.icon(@DrawableRes res: Int, init: MessageBottomSheetUMV2.Icon.() -> Unit = {}) =
apply {
icon = MessageBottomSheetUMV2.Icon(res).apply(init)
}
fun MessageBottomSheetUMV2.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUMV2.Chip.() -> Unit = {}) =
apply {
chip = MessageBottomSheetUMV2.Chip(text).apply(init)
}
internal fun MessageBottomSheetUMV2.button(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
val element = MessageBottomSheetUMV2.Button().apply(init)
elements = (elements + element).toPersistentList()
}
fun MessageBottomSheetUMV2.primaryButton(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
button { isPrimary = true; apply(init) }
}
fun MessageBottomSheetUMV2.secondaryButton(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.Button.() -> Unit) =
apply {
button { isPrimary = false; apply(init) }
}
fun MessageBottomSheetUMV2.Button.onClick(block: MessageBottomSheetUMV2.CloseScope.() -> Unit) = apply {
onClick = block
}
// endregion

View file

@ -0,0 +1,238 @@
package com.tangem.core.ui.components.bottomsheets.message
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.MessageBottomSheetUMV2.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.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Composable
fun MessageBottomSheetV2(state: MessageBottomSheetUMV2, onDismissRequest: () -> Unit) {
val config = TangemBottomSheetConfig(
isShown = true,
content = state,
onDismissRequest = onDismissRequest,
)
TangemModalBottomSheet(
config,
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = state.onDismissRequest,
)
},
) { content: MessageBottomSheetUMV2 ->
MessageBottomSheetV2Content(content.copy(onDismissRequest = onDismissRequest))
}
}
@Composable
private fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.elements.fastForEach {
when (it) {
is MessageBottomSheetUMV2.InfoBlock -> {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
state = it,
)
}
else -> Unit
}
}
ButtonsContainer(
modifier = Modifier.fillMaxWidth(),
closeScope = state.closeScope,
buttons = state.elements.filterIsInstance<MessageBottomSheetUMV2.Button>().toPersistentList(),
)
}
}
@Composable
private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
state.icon?.let {
BottomSheetIcon(it)
}
state.title?.let {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
text = it.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
state.body?.let {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
state.chip?.let {
BottomSheetChip(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing16),
chip = it,
)
}
}
}
@Composable
private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifier = Modifier) {
val tint = when (icon.type) {
MessageBottomSheetUMV2.Icon.Type.Unspecified -> Color.Unspecified
MessageBottomSheetUMV2.Icon.Type.Accent -> TangemTheme.colors.icon.accent
MessageBottomSheetUMV2.Icon.Type.Informative -> TangemTheme.colors.icon.informative
MessageBottomSheetUMV2.Icon.Type.Attention -> TangemTheme.colors.icon.attention
MessageBottomSheetUMV2.Icon.Type.Warning -> TangemTheme.colors.icon.warning
}
val backgroundColor = when (icon.backgroundType) {
MessageBottomSheetUMV2.Icon.BackgroundType.Unspecified -> TangemTheme.colors.icon.informative
MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint -> tint
}
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(backgroundColor.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(icon.res),
contentDescription = null,
tint = tint,
)
},
)
}
@Composable
private fun BottomSheetChip(chip: MessageBottomSheetUMV2.Chip, modifier: Modifier = Modifier) {
val color = when (chip.type) {
MessageBottomSheetUMV2.Chip.Type.Unspecified -> TangemTheme.colors.text.primary1
MessageBottomSheetUMV2.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<MessageBottomSheetUMV2.Button>,
closeScope: MessageBottomSheetUMV2.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 {
when (button.iconOrder) {
IconOrder.Start -> TangemButtonIconPosition.Start(it)
IconOrder.End -> TangemButtonIconPosition.End(it)
}
} ?: TangemButtonIconPosition.None
TangemButton(
modifier = Modifier.fillMaxWidth(),
text = button.text?.resolveReference() ?: "",
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
@Composable
private fun Preview() {
TangemThemePreview {
MessageBottomSheetV2(
messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
type = MessageBottomSheetUMV2.Icon.Type.Attention
backgroundType = MessageBottomSheetUMV2.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 = {},
)
}
}

View file

@ -4,6 +4,9 @@ import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.ui.UiMessage
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2Dsl
import com.tangem.core.ui.components.bottomsheets.message.messageBottomSheetUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage.Duration
@ -177,6 +180,13 @@ data class BottomSheetMessage(
}
}
data class BottomSheetMessageV2(
val messageBottomSheetUMV2: MessageBottomSheetUMV2,
) : EventMessage
internal fun bottomSheetMessage(init: @MessageBottomSheetV2Dsl MessageBottomSheetUMV2.() -> Unit) =
BottomSheetMessageV2(messageBottomSheetUM(init))
/**
* Represents an action button in the dialog.
*

View file

@ -11,6 +11,7 @@ import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalEventMessageHandler
@ -29,6 +30,7 @@ fun EventMessageEffect(
var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
EventEffect(event = messageEvent) { message ->
when (message) {
@ -41,6 +43,9 @@ fun EventMessageEffect(
is BottomSheetMessage -> {
bottomSheetMessage = message
}
is BottomSheetMessageV2 -> {
bottomSheetMessageV2 = message
}
}
}
@ -63,6 +68,13 @@ fun EventMessageEffect(
},
)
}
bottomSheetMessageV2?.let { message ->
MessageBottomSheetV2(
state = message.messageBottomSheetUMV2,
onDismissRequest = { bottomSheetMessageV2 = null },
)
}
}
@Composable