Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-26 11:14:52 +03:00
parent 2cd0164d1d
commit 5016ee56ea
10 changed files with 1798 additions and 1560 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
package com.tangem.core.ui.components.notifications
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Currency notification component from Design system.
*
* @param config component config
* @param modifier modifier
* @param containerColor container color
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=3251-3466&t=eZb7ZPoNE8pQw45Y-4"
* >Figma component</a>
*/
@Composable
fun CurrencyNotification(
config: CurrencyNotificationConfig,
modifier: Modifier = Modifier,
containerColor: Color? = null,
) {
NotificationBaseContainer(
buttonsState = config.buttonsState,
onClick = null,
onCloseClick = null,
modifier = modifier,
containerColor = containerColor,
) {
MainContent(
tokenIconState = config.tokenIconState,
title = config.title,
subtitle = config.subtitle,
)
}
}
@Composable
private fun MainContent(
tokenIconState: TokenIconState,
title: TextReference,
subtitle: CurrencyNotificationConfig.AnnotatedSubtitle,
) {
Row {
TokenIcon(
state = tokenIconState,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
SpacerW(width = TangemTheme.dimens.spacing6)
TextsBlock(title = title, subtitle = subtitle)
}
}
@Composable
private fun TextsBlock(title: TextReference, subtitle: CurrencyNotificationConfig.AnnotatedSubtitle) {
Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) {
Text(
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.button,
)
val subtitleValue = subtitle.valueProvider()
ClickableText(
text = subtitleValue,
onClick = { subtitle.onClick(subtitleValue, it) },
style = TangemTheme.typography.caption2,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_Notification() {
TangemThemePreview {
CurrencyNotification(
config = CurrencyNotificationConfig(
title = resourceReference(
R.string.express_exchange_notification_refund_title,
wrappedList("USDT", "Polygon"),
),
subtitle = CurrencyNotificationConfig.AnnotatedSubtitle(
valueProvider = {
buildAnnotatedString {
append("Your transaction amount was refunded in USDT to your wallet due to OKX")
}
},
onClick = { _, _ -> },
),
tokenIconState = TokenIconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/usd-coin.png",
networkBadgeIconResId = R.drawable.ic_polygon_22,
isGrayscale = false,
showCustomBadge = false,
fallbackTint = Color(1.0f, 1.0f, 1.0f, 1.0f, ColorSpaces.Srgb),
fallbackBackground = Color(0.23529412f, 0.28627452f, 0.6117647f, 1.0f, ColorSpaces.Srgb),
),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = stringReference("Go to token"),
onClick = {},
),
),
)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.core.ui.components.notifications
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.TextReference
/**
* Currency notification component state
*
* @property title title
* @property subtitle subtitle
* @property buttonsState buttons state
* @property tokenIconState token icon state
*
[REDACTED_AUTHOR]
*/
data class CurrencyNotificationConfig(
val title: TextReference,
val subtitle: AnnotatedSubtitle,
val tokenIconState: TokenIconState,
val buttonsState: NotificationConfig.ButtonsState,
) {
/**
* Subtitle as [AnnotatedString]
*
* @property valueProvider composable function that provides [AnnotatedString]
* @property onClick lambda be invoked when text in specified position is clicked
*/
data class AnnotatedSubtitle(
val valueProvider: @Composable () -> AnnotatedString,
val onClick: (value: AnnotatedString, position: Int) -> Unit,
)
}

View file

@ -30,17 +30,19 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState
/**
* Notification component from Design system.
* Use this for Notification with title, subtitle, clickable or not.
*
* @param config component config
* @param modifier modifier
* @param iconTint icon tint
* @param config component config
* @param modifier modifier
* @param containerColor container color
* @param iconTint icon tint
* @param isEnabled flag that defines if component is clickable
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0"
* >Figma component</a>
@ -53,44 +55,33 @@ fun Notification(
iconTint: Color? = null,
isEnabled: Boolean = true,
) {
BaseContainer(
NotificationBaseContainer(
buttonsState = config.buttonsState,
onClick = config.onClick,
onCloseClick = config.onCloseClick,
modifier = modifier,
containerColor = containerColor,
isEnabled = isEnabled,
) {
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
) {
MainContent(
iconResId = config.iconResId,
iconTint = iconTint,
title = config.title,
subtitle = config.subtitle,
isClickableComponent = isEnabled && config.onClick != null,
)
Buttons(state = config.buttonsState, isEnabled = isEnabled)
}
CloseableIconButton(
onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
isEnabled = isEnabled,
MainContent(
iconResId = config.iconResId,
iconTint = iconTint,
title = config.title,
subtitle = config.subtitle,
isClickableComponent = isEnabled && config.onClick != null,
)
}
}
@Composable
private fun BaseContainer(
internal fun NotificationBaseContainer(
buttonsState: NotificationConfig.ButtonsState?,
onClick: (() -> Unit)?,
onCloseClick: (() -> Unit)?,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
containerColor: Color? = null,
content: @Composable BoxScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
val tempContainerColor by rememberUpdatedState(
newValue = if (buttonsState != null || onClick != null) {
@ -109,7 +100,22 @@ private fun BaseContainer(
shape = TangemTheme.shapes.roundedCornersXMedium,
color = containerColor ?: tempContainerColor,
) {
Box(content = content)
Box {
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
) {
content()
Buttons(state = buttonsState, isEnabled = isEnabled)
}
CloseableIconButton(
onClick = onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
isEnabled = isEnabled,
)
}
}
}

View file

@ -1,38 +1,43 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.CurrencyNotificationConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.tokendetails.impl.R
@Immutable
internal sealed class ExchangeStatusNotifications(val config: NotificationConfig) {
internal sealed interface ExchangeStatusNotifications {
data class NeedVerification(
val onGoToProviderClick: () -> Unit,
) : ExchangeStatusNotifications(
sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications
data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification(
config = NotificationConfig(
title = TextReference.Res(R.string.express_exchange_notification_verification_title),
subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text),
title = resourceReference(R.string.express_exchange_notification_verification_title),
subtitle = resourceReference(R.string.express_exchange_notification_verification_text),
iconResId = R.drawable.ic_alert_triangle_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = TextReference.Res(R.string.common_go_to_provider),
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
),
)
data class Failed(
val onGoToProviderClick: () -> Unit,
) : ExchangeStatusNotifications(
data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification(
config = NotificationConfig(
title = TextReference.Res(R.string.express_exchange_notification_failed_title),
subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text),
title = resourceReference(R.string.express_exchange_notification_failed_title),
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = TextReference.Res(R.string.common_go_to_provider),
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
),
@ -40,16 +45,48 @@ internal sealed class ExchangeStatusNotifications(val config: NotificationConfig
data class TokenRefunded(
val cryptoCurrency: CryptoCurrency,
val onReadMoreClick: () -> Unit,
val onGoToTokenClick: () -> Unit,
) : ExchangeStatusNotifications(
config = NotificationConfig(
title = stringReference("TITLE FOR TOKEN REFUND"),
subtitle = stringReference("SUBTITLE FOR TOKEN REFUND"),
iconResId = R.drawable.ic_alert_triangle_20,
) : ExchangeStatusNotifications {
val config = CurrencyNotificationConfig(
title = resourceReference(
id = R.string.express_exchange_notification_refund_title,
formatArgs = wrappedList(cryptoCurrency.symbol, cryptoCurrency.network.name),
),
subtitle = CurrencyNotificationConfig.AnnotatedSubtitle(
valueProvider = {
val linkText = stringResource(R.string.common_read_more)
val fullString = stringResource(
R.string.express_exchange_notification_refund_text,
cryptoCurrency.symbol,
linkText,
)
val linkTextPosition = fullString.length - linkText.length
buildAnnotatedString {
withStyle(SpanStyle(TangemTheme.colors.text.tertiary)) {
append(fullString.substring(0, linkTextPosition))
}
withStyle(SpanStyle(TangemTheme.colors.text.accent)) {
append(fullString.substring(linkTextPosition, fullString.length))
}
}
},
onClick = { value, position ->
val readMoreStyle = requireNotNull(value.spanStyles.getOrNull(1))
if (position in readMoreStyle.start..readMoreStyle.end) {
onReadMoreClick()
}
},
),
tokenIconState = CryptoCurrencyToIconStateConverter().convert(cryptoCurrency),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = stringReference("Go to token"),
text = resourceReference(R.string.common_go_to_token),
onClick = onGoToTokenClick,
),
),
)
)
}
}

View file

@ -25,6 +25,7 @@ import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import java.util.Locale
internal class TokenDetailsSwapTransactionsStateConverter(
private val clickIntents: TokenDetailsClickIntents,
@ -163,9 +164,11 @@ internal class TokenDetailsSwapTransactionsStateConverter(
if (refundToken == null) {
null
} else {
ExchangeStatusNotifications.TokenRefunded(refundToken) {
clickIntents.onGoToRefundedTokenClick(refundToken)
}
ExchangeStatusNotifications.TokenRefunded(
cryptoCurrency = refundToken,
onReadMoreClick = { clickIntents.onOpenUrlClick(url = getAboutCrossChainBridgesLink()) },
onGoToTokenClick = { clickIntents.onGoToRefundedTokenClick(refundToken) },
)
}
}
else -> null
@ -316,4 +319,12 @@ internal class TokenDetailsSwapTransactionsStateConverter(
isDone = isSendingDone,
)
}
private fun getAboutCrossChainBridgesLink(): String {
return if (Locale.getDefault().country == "RU") {
"https://tangem.com/ru/blog/post/an-overview-of-cross-chain-bridges/"
} else {
"https://tangem.com/en/blog/post/an-overview-of-cross-chain-bridges/"
}
}
}

View file

@ -17,11 +17,12 @@ import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.CurrencyNotification
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
@Composable
internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
@ -29,16 +30,13 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExchangeStatusBottomSheetConfig ->
ExchangeStatusBottomSheetContent(content = content)
ExchangeStatusBottomSheetContent(config = content.value)
}
}
@Composable
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
val config = content.value
Column(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_title),
@ -80,25 +78,39 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
showLink = config.showProviderLink,
onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) },
)
AnimatedContent(
targetState = config.notification,
label = "Exchange Status Notification Change",
) {
it?.let {
val tint = when (config.activeStatus) {
ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention
ExchangeStatus.Failed -> TangemTheme.colors.icon.warning
else -> null
}
Notification(
config = it.config,
iconTint = tint,
if (config.notification != null) {
Notification(state = config.notification, activeStatus = config.activeStatus)
}
SpacerH24()
}
}
@Composable
private fun Notification(state: ExchangeStatusNotifications, activeStatus: ExchangeStatus?) {
AnimatedContent(
targetState = state,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
label = "Exchange Status Notification Change",
) { notification ->
when (notification) {
is ExchangeStatusNotifications.CommonNotification -> {
com.tangem.core.ui.components.notifications.Notification(
config = notification.config,
iconTint = when (activeStatus) {
ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention
ExchangeStatus.Failed -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
is ExchangeStatusNotifications.TokenRefunded -> {
CurrencyNotification(
config = notification.config,
containerColor = TangemTheme.colors.background.action,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
}
}
SpacerH24()
}
}

View file

@ -57,4 +57,6 @@ interface TokenDetailsClickIntents {
fun onAssociateClick()
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
}

View file

@ -726,6 +726,10 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openTokenDetails(userWalletId, cryptoCurrency)
}
override fun onOpenUrlClick(url: String) {
router.openUrl(url)
}
override fun onSwapPromoDismiss() {
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()