Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-26 18:22:31 +05:00
commit b40934f2b7
14 changed files with 1168 additions and 0 deletions

View file

@ -49,6 +49,31 @@ fun Modifier.hazeEffectTangem(
}
}
/**
* Applies a haze foreground effect to the [Modifier] with consideration of power saving mode.
*
* @param style The [HazeStyle] to apply. Defaults to [HazeStyle.Unspecified].
* @param isBlurEnabled A Boolean indicating whether blur is enabled. Defaults to true.
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze foreground effect applied.
*/
@Composable
fun Modifier.hazeForegroundEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
isBlurEnabled: Boolean = true,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val isGlobalBlurEnabled = isBlurEnabled && !powerSavingEnabled.value
return hazeEffect(
style = style,
) {
blurEnabled = isGlobalBlurEnabled
configure()
}
}
/**
* Applies a haze source to the [Modifier] using the current global haze state.
*/

View file

@ -20,6 +20,27 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Accent Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8004-26798)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun AccentTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
AccentTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
}
/**
* [Accent Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8004-26798)
*

View file

@ -19,6 +19,26 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Ghost Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4804)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun GhostTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
GhostTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
)
}
/**
* [Ghost Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4804)
*

View file

@ -21,6 +21,27 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Outline Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4800)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun OutlineTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
OutlineTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
}
/**
* [Outline Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4800)
*

View file

@ -20,6 +20,27 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Primary Inverse Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=7545-78314)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun PrimaryInverseTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
PrimaryInverseTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
}
/**
* [Primary Inverse Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=7545-78314)
*

View file

@ -20,6 +20,28 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Primary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4732&t=euYo1qCxPlQl3Fa6-4)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun PrimaryTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
PrimaryTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
descriptionText = buttonUM.descriptionText,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
}
/**
* [Primary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4732&t=euYo1qCxPlQl3Fa6-4)
*
@ -40,6 +62,7 @@ fun PrimaryTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
descriptionText: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
@ -66,6 +89,7 @@ fun PrimaryTangemButton(
.clip(shape.toShape(size))
.then(backgroundModifier),
text = text,
descriptionText = descriptionText,
contentColor = contentColor,
enabled = enabled,
size = size,

View file

@ -20,6 +20,27 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Secondary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4796)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun SecondaryTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
SecondaryTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
}
/**
* [Secondary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4796)
*

View file

@ -0,0 +1,86 @@
package com.tangem.core.ui.ds.button
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Tangem button that selects the appropriate button style based on the provided [TangemButtonUM].
* [Button]("https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8320-14720&m=dev)
*
* @param buttonUM TangemButtonUM data model containing button properties.
* @param modifier Modifier to be applied to the button.
*
* @see [TangemButtonInternal] for the underlying implementation.
*/
@Suppress("LongMethod")
@Composable
fun TangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
when (buttonUM.type) {
TangemButtonType.Primary -> PrimaryTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
descriptionText = buttonUM.descriptionText,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Secondary -> SecondaryTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Accent -> AccentTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Outline -> OutlineTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.PrimaryInverse -> PrimaryInverseTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Ghost -> GhostTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
size = buttonUM.size,
state = buttonUM.state,
)
}
}

View file

@ -52,6 +52,7 @@ internal fun TangemButtonInternal(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
descriptionText: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
@ -100,6 +101,24 @@ internal fun TangemButtonInternal(
)
}
AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
val wrappedText = remember(this) { requireNotNull(descriptionText) }
val textStyle = TangemTheme.typography2.captionSemibold12
Text(
text = wrappedText.resolveReference(),
style = textStyle,
color = TangemTheme.colors2.text.status.disabled,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
)
}
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),

View file

@ -0,0 +1,43 @@
package com.tangem.core.ui.ds.button
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
/**
* Data model representing the properties of a Tangem button.
*
* @param text TextReference for the button label.
* @param descriptionText TextReference for the button description (optional).
* @param iconRes Drawable resource ID for the icon to be displayed in the button (optional).
* @param iconPosition Position of the icon (Start or End).
* @param isEnabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
* @param type TangemButtonType defining the style type of the button.
* @param onClick Lambda to be invoked when the button is clicked.
*
[REDACTED_AUTHOR]
*/
data class TangemButtonUM(
val text: TextReference? = null,
val descriptionText: TextReference? = null,
@DrawableRes val iconRes: Int? = null,
val iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
val isEnabled: Boolean = true,
val size: TangemButtonSize = TangemButtonSize.X15,
val state: TangemButtonState = TangemButtonState.Default,
val shape: TangemButtonShape = TangemButtonShape.Default,
val type: TangemButtonType,
val onClick: () -> Unit,
)
/** Enum class representing the style types of Tangem buttons */
enum class TangemButtonType {
Primary,
Secondary,
Accent,
Outline,
PrimaryInverse,
Ghost,
}

View file

@ -0,0 +1,514 @@
package com.tangem.core.ui.ds.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.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.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
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 androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.collections.immutable.persistentListOf
/**
* Tangem message component that displays a message based on the provided [TangemMessageUM].
* [Message](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8455-81318&m=dev)
*
* @param messageUM Data model containing message properties.
* @param modifier Modifier to be applied to the message component.
* @param content Optional composable content to be displayed alongside the title and subtitle.
*/
@Composable
fun TangemMessage(
messageUM: TangemMessageUM,
modifier: Modifier = Modifier,
content: @Composable (RowScope.() -> Unit)? = null,
) {
TangemMessage(
modifier = modifier,
title = messageUM.title,
subtitle = messageUM.subtitle,
messageEffect = messageUM.messageEffect,
isCentered = messageUM.isCentered,
content = content,
onCloseClick = messageUM.onCloseClick,
buttons = {
messageUM.buttonsUM.fastForEach { buttonUM ->
TangemButton(
buttonUM = buttonUM.tangemButtonUM,
modifier = Modifier.weight(1f),
)
}
},
)
}
/**
* Tangem message component that displays a notification based on the provided [NotificationConfig].
* [Message](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8455-81318&m=dev)
*
* @param config Configuration for the notification message.
* @param modifier Modifier to be applied to the message component.
*
* @see NotificationConfig for more details.
* @see com.tangem.core.ui.components.notifications.Notification for legacy component.
*/
@Composable
fun TangemMessage(config: NotificationConfig, modifier: Modifier = Modifier) {
val buttonState = config.buttonsState
TangemMessage(
title = config.title,
subtitle = config.subtitle,
modifier = modifier,
content = {
val iconTint = when (config.iconTint) {
NotificationConfig.IconTint.Unspecified -> null
NotificationConfig.IconTint.Accent -> TangemTheme.colors2.graphic.status.accent
NotificationConfig.IconTint.Attention -> TangemTheme.colors2.graphic.status.attention
NotificationConfig.IconTint.Warning -> TangemTheme.colors2.graphic.status.warning
}
if (iconTint == null) {
Image(
painter = painterResource(config.iconResId),
contentDescription = null,
modifier = Modifier.size(config.iconSize),
)
} else {
Icon(
imageVector = ImageVector.vectorResource(config.iconResId),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(config.iconSize),
)
}
},
buttons = if (buttonState != null) {
{
TangemMessageLegacyButtons(buttonState = buttonState)
}
} else {
null
},
)
}
/**
* Tangem message component that displays a message with optional title, subtitle, content, and buttons.
* [Message](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8455-81318&m=dev)
*
* @param modifier Modifier to be applied to the message component.
* @param title Optional title of the message.
* @param subtitle Optional subtitle of the message.
* @param messageEffect Effect to be applied to the message background.
* @param content Optional composable content to be displayed alongside the title and subtitle.
* @param buttons Optional composable buttons to be displayed below the message.
* @param isCentered Flag indicating whether the content should be centered horizontally.
*/
@Composable
fun TangemMessage(
modifier: Modifier = Modifier,
title: TextReference? = null,
subtitle: TextReference? = null,
messageEffect: TangemMessageEffect = TangemMessageEffect.None,
content: (@Composable RowScope.() -> Unit)? = null,
buttons: (@Composable RowScope.() -> Unit)? = null,
onCloseClick: (() -> Unit)? = null,
isCentered: Boolean = false,
) {
val alignment = if (isCentered) {
Alignment.CenterHorizontally
} else {
Alignment.Start
}
Box(modifier = modifier) {
Box(
modifier = Modifier
.matchParentSize()
.messageEffectBackground(
messageEffect = messageEffect,
radius = TangemTheme.dimens2.x6,
),
)
Column(
horizontalAlignment = alignment,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
modifier = Modifier
.padding(TangemTheme.dimens2.x3)
.fillMaxWidth(),
) {
TangemMessageContent(
title = title,
subtitle = subtitle,
alignment = alignment,
content = content,
)
if (buttons != null && !isCentered) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier
.fillMaxWidth(),
) {
buttons()
}
}
}
if (onCloseClick != null) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_close_new_20),
contentDescription = null,
tint = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x5)
.clickableSingle(onClick = onCloseClick),
)
}
}
}
@Composable
private fun TangemMessageContent(
title: TextReference? = null,
subtitle: TextReference? = null,
alignment: Alignment.Horizontal = Alignment.Start,
content: (@Composable RowScope.() -> Unit)? = null,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
modifier = Modifier.padding(TangemTheme.dimens2.x1),
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = alignment,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
if (title != null) {
Text(
text = title.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
}
if (subtitle != null) {
Text(
text = subtitle.resolveAnnotatedReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
)
}
}
content?.invoke(this)
}
}
@Composable
private fun RowScope.TangemMessageLegacyButtons(buttonState: ButtonsState) {
when (buttonState) {
is ButtonsState.PrimaryButtonConfig -> PrimaryTangemButton(
text = buttonState.text,
descriptionText = buttonState.additionalText,
iconRes = buttonState.iconResId,
onClick = buttonState.onClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
state = if (buttonState.shouldShowProgress) {
TangemButtonState.Loading
} else {
TangemButtonState.Default
},
modifier = Modifier.weight(1f),
)
is ButtonsState.SecondaryButtonConfig -> PrimaryInverseTangemButton(
text = buttonState.text,
iconRes = buttonState.iconResId,
onClick = buttonState.onClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
state = if (buttonState.shouldShowProgress) {
TangemButtonState.Loading
} else {
TangemButtonState.Default
},
modifier = Modifier.weight(1f),
)
is ButtonsState.PairButtonsConfig -> {
PrimaryInverseTangemButton(
text = buttonState.primaryText,
onClick = buttonState.onPrimaryClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
modifier = Modifier.weight(1f),
)
PrimaryTangemButton(
text = buttonState.secondaryText,
onClick = buttonState.onSecondaryClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
modifier = Modifier.weight(1f),
)
}
is ButtonsState.SecondaryPairButtonsConfig -> {
PrimaryInverseTangemButton(
text = buttonState.leftText,
onClick = buttonState.onLeftClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
modifier = Modifier.weight(1f),
)
PrimaryInverseTangemButton(
text = buttonState.rightText,
onClick = buttonState.onRightClick,
iconPosition = TangemButtonIconPosition.End,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
modifier = Modifier.weight(1f),
)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemMessage_Preview(@PreviewParameter(TangemMessagePreviewProvider::class) params: TangemMessageUM) {
TangemThemePreviewRedesign {
TangemMessage(
messageUM = params,
modifier = Modifier.fillMaxWidth(),
)
}
}
private class TangemMessagePreviewProvider : PreviewParameterProvider<TangemMessageUM> {
override val values: Sequence<TangemMessageUM>
get() = sequenceOf(
TangemMessageUM(
title = stringReference("Title text"),
subtitle = stringReference("Subtext"),
messageEffect = TangemMessageEffect.None,
isCentered = true,
),
TangemMessageUM(
title = stringReference("Title text"),
subtitle = stringReference("Subtext"),
messageEffect = TangemMessageEffect.Magic,
isCentered = false,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = stringReference("Button"),
type = TangemButtonType.PrimaryInverse,
onClick = {},
),
TangemMessageButtonUM(
text = stringReference("Button"),
type = TangemButtonType.Primary,
iconRes = R.drawable.ic_tangem_24,
onClick = {},
),
),
),
TangemMessageUM(
title = stringReference("Title text"),
subtitle = stringReference("Subtext"),
messageEffect = TangemMessageEffect.Card,
isCentered = false,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = stringReference("Button"),
type = TangemButtonType.Primary,
onClick = {},
),
),
),
TangemMessageUM(
title = stringReference("Title text"),
subtitle = stringReference("Subtext"),
messageEffect = TangemMessageEffect.Warning,
isCentered = true,
onCloseClick = {},
),
)
}
// endregion
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemMessage2_Preview() {
TangemThemePreviewRedesign {
TangemMessage(
title = stringReference("Title text"),
subtitle = stringReference("Subtext"),
messageEffect = TangemMessageEffect.Magic,
isCentered = false,
content = {
Box(
modifier = Modifier
.size(TangemTheme.dimens2.x10)
.clip(RoundedCornerShape(TangemTheme.dimens2.x2))
.flicker(isFlickering = true)
.background(TangemTheme.colors2.text.neutral.primary),
)
},
buttons = {
PrimaryInverseTangemButton(
text = stringReference("Button"),
onClick = {},
modifier = Modifier.weight(1f),
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
)
PrimaryTangemButton(
text = stringReference("Button"),
onClick = {},
modifier = Modifier.weight(1f),
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
)
},
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(TangemTheme.dimens2.x1)
.fillMaxWidth(),
)
}
}
// endregion
// region Preview Legacy
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemMessageLegacy_Preview(
@PreviewParameter(TangemMessageLegacyPreviewProvider::class) params: NotificationConfig,
) {
TangemThemePreviewRedesign {
TangemMessage(
config = params,
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(TangemTheme.dimens2.x1)
.fillMaxWidth(),
)
}
}
private class TangemMessageLegacyPreviewProvider : PreviewParameterProvider<NotificationConfig> {
override val values: Sequence<NotificationConfig>
get() = sequenceOf(
NotificationConfig(
title = TextReference.Str(value = "Development card"),
subtitle = TextReference.Str(
value = "The card you scanned is a development card.\nDont accept it as a payment.",
),
iconResId = R.drawable.ic_alert_circle_24,
),
NotificationConfig(
title = TextReference.Str(value = "Some networks are unreachable"),
subtitle = TextReference.Str(value = "Check your network connection"),
iconResId = R.drawable.img_attention_20,
),
NotificationConfig(
title = TextReference.Str(value = "Used card"),
subtitle = TextReference.Str(value = "The card signed transactions in the past"),
iconResId = R.drawable.ic_alert_circle_24,
onClick = {},
),
NotificationConfig(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = TextReference.Str(
value = "To protect your assets, we advise you to carry out this procedure",
),
iconResId = R.drawable.img_attention_20,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = TextReference.Str(value = "Start backup process"),
onClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Some addresses are missing"),
subtitle = TextReference.Str(value = "Generate addresses for 2 new networks using your card"),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = ButtonsState.PrimaryButtonConfig(
text = TextReference.Str(value = "Generate addresses"),
iconResId = R.drawable.ic_tangem_24,
onClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Rate the app"),
subtitle = TextReference.Str(value = "How do you like Tangem?"),
iconResId = R.drawable.img_attention_20,
buttonsState = ButtonsState.PairButtonsConfig(
primaryText = TextReference.Str(value = "Love it!"),
onPrimaryClick = {},
secondaryText = TextReference.Str(value = "Can be better"),
onSecondaryClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Rate the app"),
subtitle = TextReference.Str(value = "How do you like Tangem?"),
iconResId = R.drawable.img_attention_20,
buttonsState = ButtonsState.SecondaryPairButtonsConfig(
leftText = TextReference.Str(value = "Love it!"),
onLeftClick = {},
rightText = TextReference.Str(value = "Can be better"),
onRightClick = {},
),
),
NotificationConfig(
title = TextReference.Str(value = "Note top up"),
subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = TextReference.Str(value = "Top up card"),
onClick = {},
),
onCloseClick = {},
),
NotificationConfig(
subtitle = resourceReference(id = R.string.information_generated_with_ai),
iconResId = R.drawable.ic_magic_28,
),
NotificationConfig(
title = resourceReference(R.string.notification_sepa_title),
subtitle = resourceReference(R.string.notification_sepa_text),
iconResId = R.drawable.img_notification_sepa,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_sepa_button),
onClick = { },
),
iconSize = 54.dp,
),
)
}
// endregion

View file

@ -0,0 +1,292 @@
package com.tangem.core.ui.ds.message
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Different visual effects for [Tangem message component].
*/
enum class TangemMessageEffect {
/** Magic effect with vibrant colors */
Magic,
/** Card effect with bright colors */
Card,
/** Warning effect with alert colors */
Warning,
/** No special effect */
None,
;
/** Gets the color gradient based on the effect type and [isInDarkTheme] */
@Suppress("MagicNumber")
fun getColorGradient(isInDarkTheme: Boolean): ImmutableList<Color> {
return when (this) {
Magic -> if (isInDarkTheme) {
persistentListOf(
Color(0x9976E463),
Color(0x994A40D3),
Color(0x99CC4D8C),
Color(0x99E0BC56),
)
} else {
persistentListOf(
Color(0xFF76E463),
Color(0xFF4A40D3),
Color(0xFFCC4D8C),
Color(0xFFE0BC56),
)
}
Card -> if (isInDarkTheme) {
persistentListOf(
Color(0x99D21FF2),
Color(0x991319F3),
Color(0x99FF005D),
Color(0x99EAB216),
)
} else {
persistentListOf(
Color(0xFFD21FF2),
Color(0xFF1319F3),
Color(0xFFFF005D),
Color(0xFFEAB216),
)
}
Warning -> if (isInDarkTheme) {
persistentListOf(
Color(0x59FF0004),
Color(0x59FF0004),
Color(0x59E0BC56),
Color(0x59FF0000),
Color(0x59FFACC1),
Color(0x59FFACC1),
)
} else {
persistentListOf(
Color(0x4DFF0004),
Color(0x4DFF0004),
Color(0x4DE0BC56),
Color(0x4DFF0000),
Color(0x4DFFACC1),
Color(0x4DFFACC1),
)
}
None -> if (isInDarkTheme) {
persistentListOf(
Color(0x1AFFFFFF),
Color(0x1AFFFFFF),
)
} else {
persistentListOf()
}
}
}
/** Gets the gradient tints based on the effect type and [isInDarkTheme] */
@Suppress("MagicNumber")
fun getGradientTint(isInDarkTheme: Boolean) = when (this) {
Magic,
Card,
-> {
listOf(
HazeTint(
color = Color(0x4DFFFFFF),
blendMode = BlendMode.Color,
),
HazeTint(
color = Color(0x80C2C2C2),
blendMode = BlendMode.Overlay,
),
HazeTint(
color = Color(0x1FC2C2C2),
),
)
}
Warning -> {
if (isInDarkTheme) {
listOf(
HazeTint(
color = Color(0x4D7F7F7F),
blendMode = BlendMode.Luminosity,
),
HazeTint(
color = Color(0x80C2C2C2),
blendMode = BlendMode.Overlay,
),
HazeTint(
Color(0x1FFF0000),
),
)
} else {
listOf(
HazeTint(
color = Color(0x0DFEC2C2),
),
)
}
}
None -> {
listOf(
HazeTint(
color = if (isInDarkTheme) Color(0x1AFFFFFF) else Color.Transparent,
),
)
}
}
/** Gets the border gradient based on the effect type and [isInDarkTheme] */
@Suppress("MagicNumber")
fun getBorderGradient(isInDarkTheme: Boolean): ImmutableList<Color> {
return when (this) {
Card,
Magic,
-> if (isInDarkTheme) {
persistentListOf(
Color(0x4DFFFFFF),
Color(0x17FFFFFF),
Color(0x4DFFFFFF),
Color(0x17FFFFFF),
)
} else {
persistentListOf(
Color(0x0d000000),
Color(0x26000000),
Color(0x0d000000),
Color(0x26000000),
)
}
Warning -> persistentListOf(
Color(0x4DE44848),
Color(0x17E44848),
Color(0x4DE44848),
Color(0x17E44848),
)
None -> if (isInDarkTheme) {
persistentListOf()
} else {
persistentListOf(
Color(0x0d000000),
Color(0x26000000),
Color(0x0d000000),
Color(0x26000000),
)
}
}
}
/** Gets the border color based on the effect type and [isInDarkTheme] */
@Suppress("MagicNumber")
fun getBorderColor(isInDarkTheme: Boolean): Color {
return when (this) {
Card,
Magic,
None,
-> if (isInDarkTheme) Color(0x1AFFFFFF) else Color.Transparent
Warning -> Color(0x1AE44848)
}
}
// todo redesign replace with proper color when design is ready
@Suppress("MagicNumber")
fun fallbackColor(isInDarkTheme: Boolean): Color {
return when (this) {
Card -> Color(0x339B8B86)
Magic -> Color(0x33B43B96)
Warning -> if (isInDarkTheme) Color(0x33FF0000) else Color(0x33FF0000)
None -> if (isInDarkTheme) Color(0x1AFFFFFF) else Color.Transparent
}
}
}
/** Applies a message effect background to the [Modifier] based on the provided [messageEffect] and [radius] */
@Composable
internal fun Modifier.messageEffectBackground(messageEffect: TangemMessageEffect, radius: Dp): Modifier {
val isInDarkTheme = LocalIsInDarkTheme.current
val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) }
val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) }
return this
.clip(RoundedCornerShape(radius))
.border(
width = 1.dp,
color = messageEffect.getBorderColor(isInDarkTheme),
shape = RoundedCornerShape(radius),
)
.conditionalCompose(borderGradientColors.isNotEmpty()) {
border(
width = 1.dp,
brush = Brush.sweepGradient(
colors = messageEffect.getBorderGradient(isInDarkTheme),
center = Offset.Infinite,
),
shape = RoundedCornerShape(radius),
)
}
.hazeForegroundEffectTangem(
style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)),
isBlurEnabled = true,
) {
fallbackTint = HazeTint(
messageEffect.fallbackColor(isInDarkTheme),
)
blurRadius = 25.dp
}
.conditionalCompose(gradientColors.isNotEmpty()) {
border(
width = 0.dp,
brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)),
shape = RoundedCornerShape(radius),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemMessageEffect_Preview() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
TangemMessageEffect.entries.forEach { messageEffect ->
Box(
modifier = Modifier
.messageEffectBackground(
messageEffect = messageEffect,
radius = 16.dp,
)
.fillMaxWidth()
.height(height = 100.dp),
)
}
}
}
}
// endregion

View file

@ -0,0 +1,52 @@
package com.tangem.core.ui.ds.message
import androidx.annotation.DrawableRes
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Data model representing the properties of a Tangem message.
*
* @param title TextReference for the message title.
* @param subtitle TextReference for the message subtitle.
* @param messageEffect TangemMessageEffect defining the visual effect of the message.
* @param isCentered Boolean indicating whether the icon is centered.
* @param buttonsUM ImmutableList of TangemMessageButtonUM representing the buttons in the message.
* @param onCloseClick Lambda to be invoked when the close button is clicked (optional
*/
data class TangemMessageUM(
val title: TextReference,
val subtitle: TextReference,
val messageEffect: TangemMessageEffect,
val isCentered: Boolean,
val buttonsUM: ImmutableList<TangemMessageButtonUM> = persistentListOf(),
val onCloseClick: (() -> Unit)? = null,
)
/**
* Data model representing a button within a Tangem message.
*
* @param text TextReference for the button label.
* @param type TangemButtonType defining the style type of the button.
* @param iconRes Drawable resource ID for the icon to be displayed in the button (optional).
* @param onClick Lambda to be invoked when the button is clicked.
*/
data class TangemMessageButtonUM(
val text: TextReference,
val type: TangemButtonType,
@DrawableRes val iconRes: Int? = null,
val onClick: () -> Unit,
) {
/** Creates a TangemButtonUM representation of this message button. */
val tangemButtonUM = TangemButtonUM(
text = text,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
iconRes = iconRes,
iconPosition = TangemButtonIconPosition.End,
type = type,
onClick = onClick,
)
}

View 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="M9.992,2C14.369,2 18,5.624 18,10C18,14.369 14.376,18 10,18C5.631,18 2,14.369 2,10C2,5.624 5.624,2 9.992,2ZM12.651,6.722C12.479,6.722 12.337,6.777 12.204,6.91L10,9.098L7.812,6.918C7.686,6.8 7.545,6.737 7.364,6.737C7.004,6.737 6.722,7.012 6.722,7.381C6.722,7.553 6.785,7.71 6.91,7.835L9.082,10.008L6.91,12.189C6.785,12.306 6.722,12.471 6.722,12.644C6.722,13.004 7.004,13.294 7.364,13.294C7.545,13.294 7.709,13.231 7.835,13.106L10,10.926L12.173,13.106C12.29,13.231 12.455,13.294 12.635,13.294C13.003,13.294 13.294,13.004 13.294,12.644C13.294,12.463 13.231,12.298 13.098,12.181L10.926,10.008L13.106,7.827C13.247,7.686 13.302,7.545 13.302,7.364C13.302,7.004 13.012,6.722 12.651,6.722Z"
android:fillColor="#656565"/>
</vector>