Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-19 17:01:05 +05:00
parent 5c8102a75a
commit 47f9a36fb4
19 changed files with 115 additions and 93 deletions

View file

@ -19,6 +19,7 @@ dependencies {
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
/** Deps */
implementation(deps.kotlin.immutable.collections)
@ -34,6 +35,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.onramp.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {

View file

@ -0,0 +1,71 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.res.stringResource
import com.tangem.common.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.inputrow.InputRowApprox
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Suppress("LongParameterList")
@Composable
fun ExpressEstimate(
timestamp: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,
fromCryptoAmount: TextReference,
fromCryptoSymbol: String,
toCryptoAmount: TextReference,
toCryptoSymbol: String,
fromFiatAmount: TextReference?,
toFiatAmount: TextReference?,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing2,
),
) {
Text(
text = stringResource(id = R.string.express_estimated_amount),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = timestamp.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
InputRowApprox(
leftIcon = fromTokenIconState,
leftTitle = fromCryptoAmount,
leftSubtitle = fromFiatAmount,
leftTitleEllipsisOffset = fromCryptoSymbol.length,
rightIcon = toTokenIconState,
rightTitle = toCryptoAmount,
rightSubtitle = toFiatAmount,
rightTitleEllipsisOffset = toCryptoSymbol.length,
)
}
}

View file

@ -0,0 +1,109 @@
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun ExpressProvider(
providerName: TextReference,
providerType: TextReference,
providerTxId: String?,
imageUrl: String,
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
) {
Text(
text = stringResource(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
if (!providerTxId.isNullOrEmpty()) {
Row(
modifier = Modifier
.padding(start = 8.dp)
.clickable {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(providerTxId))
Toast.makeText(context, R.string.express_transaction_id_copied, Toast.LENGTH_SHORT).show()
},
) {
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.padding(end = TangemTheme.dimens.spacing4)
.align(Alignment.CenterVertically),
painter = painterResource(id = R.drawable.ic_copy_24),
contentDescription = null,
tint = TangemTheme.colors.text.tertiary,
)
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = stringResource(R.string.express_transaction_id, providerTxId),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
InputRowBestRate(
imageUrl = imageUrl,
title = providerName,
titleExtra = providerType,
subtitle = TextReference.Res(R.string.express_floating_rate),
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ExpressProvider_Preview() {
TangemThemePreview {
ExpressProvider(
providerName = TextReference.Str("Changelly"),
providerType = TextReference.Str("CEX"),
providerTxId = "hjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqbhjsbajcqb",
imageUrl = "",
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
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.res.TangemTheme
data class ExpressStatusBottomSheetConfig(
val value: ExpressTransactionStateUM,
) : TangemBottomSheetConfigContent
@Composable
fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
}
}
}

View file

@ -0,0 +1,191 @@
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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 androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.constraintlayout.compose.*
import com.tangem.common.ui.R
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable
internal fun ExpressStatusItem(
title: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,
fromAmount: TextReference,
fromSymbol: String,
toSymbol: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
toAmount: TextReference = TextReference.EMPTY,
@DrawableRes infoIconRes: Int? = null,
infoIconTint: Color? = null,
) {
ConstraintLayout(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.primary)
.clickable { onClick() }
.padding(TangemTheme.dimens.spacing12),
) {
val (titleRef, iconRef, infoIconRef, swapIconRef, fromRef, toRef, fromIconRef, toIconRef) = createRefs()
val padding6 = TangemTheme.dimens.spacing6
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(parent.start)
top.linkTo(parent.top)
},
)
CurrencyIcon(
state = fromTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(fromIconRef) {
start.linkTo(parent.start)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
EllipsisText(
text = fromAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
ellipsis = TextEllipsis.OffsetEnd(fromSymbol.length),
modifier = Modifier.constrainAs(fromRef) {
start.linkTo(fromIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
end.linkTo(swapIconRef.start)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints.atMostWrapContent
},
)
Icon(
painter = painterResource(id = R.drawable.ic_forward_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size12)
.constrainAs(swapIconRef) {
start.linkTo(fromRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
end.linkTo(toIconRef.start)
bottom.linkTo(parent.bottom)
},
)
CurrencyIcon(
state = toTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(toIconRef) {
start.linkTo(swapIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
end.linkTo(toRef.start)
bottom.linkTo(parent.bottom)
},
)
EllipsisText(
text = toAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
ellipsis = TextEllipsis.OffsetEnd(toSymbol.length),
modifier = Modifier.constrainAs(toRef) {
start.linkTo(toIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
end.linkTo(infoIconRef.start, padding6, padding6)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints.atLeastWrapContent
},
)
Icon(
painter = painterResource(id = infoIconRes ?: R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = infoIconTint ?: TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(infoIconRef) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(iconRef.start)
visibility = if (infoIconRes == null) {
Visibility.Gone
} else {
Visibility.Visible
}
},
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.constrainAs(iconRef) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
)
}
}
//region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ExpressStatusItemPreview(
@PreviewParameter(ExpressStatusItemPreviewParameterProvider::class) amount: String,
) {
TangemThemePreview {
ExpressStatusItem(
title = stringReference("ChangeNow"),
fromTokenIconState = CurrencyIconState.Loading,
toTokenIconState = CurrencyIconState.Loading,
fromAmount = stringReference(amount),
fromSymbol = "USDT",
toAmount = stringReference(amount),
toSymbol = "USDT",
onClick = {},
infoIconRes = null,
infoIconTint = null,
)
}
}
private class ExpressStatusItemPreviewParameterProvider : PreviewParameterProvider<String> {
override val values: Sequence<String>
get() = sequenceOf(
"1111111111111111111111111111 USDT",
"11111 USDT",
)
}
//endregion

View file

@ -0,0 +1,45 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.PersistentList
fun LazyListScope.expressTransactionsItems(
expressTxs: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier = Modifier,
) {
items(
count = expressTxs.size,
key = { expressTxs[it].info.txId },
contentType = { expressTxs[it]::class.java },
) {
val itemInfo = expressTxs[it].info
val (iconRes, tint) = when (itemInfo.iconState) {
ExpressTransactionStateIconUM.Warning -> {
R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
}
ExpressTransactionStateIconUM.Error -> {
R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
}
ExpressTransactionStateIconUM.None -> null to null
}
ExpressStatusItem(
title = itemInfo.title,
fromTokenIconState = itemInfo.fromCurrencyIcon,
toTokenIconState = itemInfo.toCurrencyIcon,
fromAmount = itemInfo.fromAmount,
fromSymbol = itemInfo.fromAmountSymbol,
toAmount = itemInfo.toAmount,
toSymbol = itemInfo.toAmountSymbol,
onClick = itemInfo.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItem(),
)
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.common.ui.expressStatus
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.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
@Composable
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(
text = stringResource(id = R.string.common_transaction_status),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.align(CenterHorizontally),
)
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(CenterHorizontally),
)
SpacerH16()
ExpressEstimate(
timestamp = state.info.timestampFormatted,
fromTokenIconState = state.info.fromCurrencyIcon,
toTokenIconState = state.info.toCurrencyIcon,
fromCryptoAmount = state.info.fromAmount,
fromCryptoSymbol = state.info.fromAmountSymbol,
toCryptoAmount = state.info.toAmount,
toCryptoSymbol = state.info.toAmountSymbol,
fromFiatAmount = state.info.fromFiatAmount,
toFiatAmount = state.info.toFiatAmount,
)
SpacerH12()
ExpressProvider(
providerName = stringReference(state.providerName),
providerType = stringReference(state.providerType),
providerTxId = state.info.txExternalId,
imageUrl = state.providerImageUrl,
)
SpacerH12()
ExpressStatusBlock(state = state.info.status)
ExpressStatusNotificationBlock(state = state.info.notification)
SpacerH24()
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.common.ui.expressStatus.state
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus
interface ExpressTransactionStateUM {
val info: ExpressTransactionStateInfoUM
data class OnrampUM(
override val info: ExpressTransactionStateInfoUM,
val providerName: String, // todo onramp fix after SwapProvider moved to own module
val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val activeStatus: OnrampStatus.Status,
val fromCurrencyCode: String,
) : ExpressTransactionStateUM
}
data class ExpressTransactionStateInfoUM(
val title: TextReference,
val status: ExpressStatusUM,
val notification: NotificationUM?,
val txId: String,
val txExternalId: String?,
val txExternalUrl: String?,
val timestamp: Long,
val timestampFormatted: TextReference,
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,
val toAmountSymbol: String,
val toCurrencyIcon: CurrencyIconState,
val fromAmount: TextReference,
val fromFiatAmount: TextReference?,
val fromAmountSymbol: String,
val fromCurrencyIcon: CurrencyIconState,
)
enum class ExpressTransactionStateIconUM {
Warning,
Error,
None,
}