diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt index 779a7a0b55..d34949ea34 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt @@ -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. */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/AccentTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/AccentTangemButton.kt index da0e951bfa..8c14fb6a7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/AccentTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/AccentTangemButton.kt @@ -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) * diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt index 16b1f51876..0fece5aae0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt @@ -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) * diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/OutlineTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/OutlineTangemButton.kt index e86db3a8b8..03486f2f74 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/OutlineTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/OutlineTangemButton.kt @@ -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) * diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryInverseTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryInverseTangemButton.kt index 8fc4f8644e..68d5b682b4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryInverseTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryInverseTangemButton.kt @@ -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) * diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryTangemButton.kt index 74487c813b..f845d30d37 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/PrimaryTangemButton.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt index 8c43685df8..95b2371aee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/SecondaryTangemButton.kt @@ -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) * diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButton.kt new file mode 100644 index 0000000000..6e8600bac6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButton.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index abc8dcf003..4d6f5dc9d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -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()), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt new file mode 100644 index 0000000000..0b0e426629 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -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, +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt new file mode 100644 index 0000000000..ff63624792 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -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 { + override val values: Sequence + 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 { + override val values: Sequence + get() = sequenceOf( + NotificationConfig( + title = TextReference.Str(value = "Development card"), + subtitle = TextReference.Str( + value = "The card you scanned is a development card.\nDon’t 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 hasn’t 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt new file mode 100644 index 0000000000..0ef086a925 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt @@ -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 { + 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 { + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt new file mode 100644 index 0000000000..80f8249c40 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt @@ -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 = 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, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_close_new_20.xml b/core/ui/src/main/res/drawable/ic_close_new_20.xml new file mode 100644 index 0000000000..e3036c1d97 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_close_new_20.xml @@ -0,0 +1,9 @@ + + +