diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 5d8b37fd46..4f4e74d559 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -31,10 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList @@ -118,7 +115,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh @Composable private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) { FooterContainer( - footer = stringResource(id = R.string.give_permission_policy_type_footer), + footer = resourceReference(R.string.give_permission_policy_type_footer), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { AmountItem( @@ -130,7 +127,7 @@ private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) } SpacerH16() FooterContainer( - footer = data.footerText.resolveReference(), + footer = data.footerText, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { FeeItem(fee = data.fee) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt new file mode 100644 index 0000000000..cdf3f1f8fb --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt @@ -0,0 +1,228 @@ +package com.tangem.common.ui.expressStatus + +import androidx.annotation.DrawableRes +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.R +import com.tangem.common.ui.expressStatus.state.ExpressLinkUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Block with express statuses + * + * @param state ui holder + * @param modifier modifier + * @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4) + */ +@Composable +fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing16), + ) { + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerWMax() + AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) { + val link = remember(this) { state.link as ExpressLinkUM.Content } + Row( + modifier = Modifier.clickable { link.onClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = link.icon), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.spacing16) + .padding(end = TangemTheme.dimens.spacing2), + ) + Text( + text = link.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + Column { + state.statuses.forEachIndexed { index, item -> + ExpressStatusStep(item, index == state.statuses.lastIndex) + } + } + } +} + +@Composable +private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) { + AnimatedContent( + targetState = status, + label = "Exchange Step Change Success", + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + ) { content -> + Row { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (content.state) { + ExpressStatusItemState.Active -> StepInProgress() + ExpressStatusItemState.Default -> StepDefault() + ExpressStatusItemState.Done -> Step( + iconRes = R.drawable.ic_check_24, + iconColor = TangemTheme.colors.icon.primary1, + borderColor = TangemTheme.colors.field.focused, + ) + ExpressStatusItemState.Error -> Step( + iconRes = R.drawable.ic_close_24, + iconColor = TangemTheme.colors.icon.warning, + ) + ExpressStatusItemState.Warning -> Step( + iconRes = R.drawable.ic_close_24, + iconColor = TangemTheme.colors.icon.attention, + ) + } + if (!isLast) { + StepSeparator() + } + } + val textColor = when (status.state) { + ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1 + ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled + ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1 + ExpressStatusItemState.Error -> TangemTheme.colors.text.warning + ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention + } + Text( + text = content.text.resolveReference(), + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun StepDefault() { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .border( + width = TangemTheme.dimens.size1_5, + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconColor, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .border( + width = TangemTheme.dimens.size1_5, + color = borderColor, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun StepInProgress() { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + modifier = Modifier + .padding(TangemTheme.dimens.spacing2) + .size(TangemTheme.dimens.size14), + ) +} + +@Composable +private fun StepSeparator() { + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .size( + width = TangemTheme.dimens.size1_5, + height = TangemTheme.dimens.size10, + ) + .background( + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ), + ) +} + +@Preview +@Composable +private fun Preview_ExchangeStatusBlock() { + val state = ExpressStatusUM( + title = resourceReference(R.string.express_exchange_status_title), + link = ExpressLinkUM.Content( + icon = R.drawable.ic_alert_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = {}, + ), + statuses = persistentListOf( + ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done), + ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active), + ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning), + ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error), + ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default), + ), + ) + + TangemThemePreview { + ExpressStatusBlock(state = state) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt new file mode 100644 index 0000000000..b93ad58334 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt @@ -0,0 +1,32 @@ +package com.tangem.common.ui.expressStatus + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.ExpressNotificationsUM +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun ExpressStatusNotificationBlock(state: NotificationUM?) { + AnimatedContent( + targetState = state, + modifier = Modifier.padding(top = 12.dp), + label = "Express Status Notification Change", + ) { notification -> + if (notification?.config != null) { + Notification( + config = notification.config, + iconTint = when (state) { + is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention + is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning + else -> null + }, + containerColor = TangemTheme.colors.background.action, + ) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt new file mode 100644 index 0000000000..6fcb02cc30 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt @@ -0,0 +1,54 @@ +package com.tangem.common.ui.expressStatus.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +/** + * UI data holder for express status block + * + * @property title block title + * @property link provider web link + * @property statuses list of possible and active statuses + */ +data class ExpressStatusUM( + val title: TextReference, + val link: ExpressLinkUM, + val statuses: ImmutableList, +) + +/** + * Provider web link for express status block. + * [Empty] if no link needed + * [Content] if link is provided and displayed + */ +@Stable +sealed class ExpressLinkUM { + data object Empty : ExpressLinkUM() + data class Content( + @DrawableRes val icon: Int, + val text: TextReference, + val onClick: () -> Unit, + ) : ExpressLinkUM() +} + +/** + * Single status item in express status block + */ +data class ExpressStatusItemUM( + val text: TextReference, + val state: ExpressStatusItemState, +) + +/** + * Available status states for express status block + */ +enum class ExpressStatusItemState { + Active, + Default, + Done, + Warning, + Error, + ; +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt new file mode 100644 index 0000000000..09c66ed8c8 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt @@ -0,0 +1,28 @@ +package com.tangem.common.ui.notifications + +import com.tangem.common.ui.R +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference + +object ExpressNotificationsUM { + + data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning( + 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 = resourceReference(R.string.common_go_to_provider), + onClick = onGoToProviderClick, + ), + ) + + data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error( + 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, + buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_provider), + onClick = onGoToProviderClick, + ), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt index 5296313d28..bebd090e66 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt @@ -5,8 +5,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme /** @@ -20,15 +23,16 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun FooterContainer( modifier: Modifier = Modifier, - footer: String? = null, + footer: TextReference? = null, footerTopPadding: Dp = TangemTheme.dimens.spacing8, content: @Composable () -> Unit, ) { Column(modifier = modifier) { content() AnimatedVisibility(visible = footer != null) { + val footerWrapped = remember(this) { requireNotNull(footer) } Text( - text = footer.orEmpty(), + text = footerWrapped.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 1af5a5154a..65ad8d5140 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.inputrow.InputRowEnterAmount import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -41,7 +40,7 @@ internal fun SendCustomFee( repeat(customValues.size) { index -> val value = customValues[index] FooterContainer( - footer = value.footer.resolveReference(), + footer = value.footer, ) { if (value.label != null) { InputRowEnterInfoAmount( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 013b9e7258..ea78244e14 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -21,8 +21,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowRecipient +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R @@ -32,8 +36,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub -import com.tangem.core.ui.components.containers.FooterContainer -import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -107,7 +109,7 @@ private fun LazyListScope.addressItem( ) { item(key = ADDRESS_FIELD_KEY) { FooterContainer( - footer = stringResource(R.string.send_recipient_address_footer, network), + footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(network)), ) { InputRowRecipient( value = address.value, @@ -137,7 +139,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM value = memoField.value, label = memoField.label, placeholder = placeholder, - footer = stringResource(R.string.send_recipient_memo_footer), + footer = resourceReference(R.string.send_recipient_memo_footer), onValueChange = memoField.onValueChange, onPasteClick = onMemoChange, modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 762dc7f7eb..cc33643eab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -27,7 +27,7 @@ internal fun TextFieldWithPaste( onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, modifier: Modifier = Modifier, - footer: String? = null, + footer: TextReference? = null, labelStyle: TextStyle = TangemTheme.typography.body2, error: TextReference? = null, isError: Boolean = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index 47508d351e..8d262a4334 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -19,6 +19,7 @@ internal sealed interface ExchangeStatusNotifications { sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications + @Deprecated("Use one in ExpressNotificationsUM") data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( title = resourceReference(R.string.express_exchange_notification_verification_title), @@ -31,6 +32,7 @@ internal sealed interface ExchangeStatusNotifications { ), ) + @Deprecated("Use one in ExpressNotificationsUM") data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( title = resourceReference(R.string.express_exchange_notification_failed_title), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index ec2a2b3cfb..7831a8aba3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -30,6 +30,7 @@ import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +@Deprecated("Use ExpressStatusBlock from common") @Composable internal fun ExchangeStatusBlock( statuses: ImmutableList,