Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-16 16:15:56 +05:00
commit 5e850ef1d4
13 changed files with 191 additions and 109 deletions

View file

@ -8,6 +8,7 @@ object StringsSigns {
const val DASH_SIGN = ""
const val LOWER_SIGN = "<"
const val TILDE_SIGN = "~"
const val COMA_SIGN = ","
const val INFINITY_SIGN = ""
const val NON_BREAKING_SPACE = '\u00A0'
const val PERCENT = "%"

View file

@ -0,0 +1,51 @@
package com.tangem.features.send.v2.common.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) }
val keyboard by keyboardAsState()
// the text should appear when the keyboard is closed
LaunchedEffect(footerText != TextReference.EMPTY, keyboard) {
if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) {
return@LaunchedEffect
}
isVisibleProxy = footerText != TextReference.EMPTY
}
AnimatedVisibility(
visible = isVisibleProxy,
modifier = modifier,
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
exit = fadeOut(tween(durationMillis = 300)),
label = "Animate show sending state text",
) {
Text(
text = footerText.resolveAnnotatedReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
)
}
}

View file

@ -12,6 +12,7 @@ internal sealed class ConfirmUM {
data class Content(
override val isPrimaryButtonEnabled: Boolean = false,
val walletName: TextReference,
val isSending: Boolean,
val showTapHelp: Boolean,
val sendingFooter: TextReference,

View file

@ -0,0 +1,66 @@
package com.tangem.features.send.v2.common.utils
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.StringsSigns.COMA_SIGN
internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference {
val suffix = when {
fee.remainingEnergy == 0L -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_including,
wrappedList(fiatFee),
)
}
fee.feeEnergy <= fee.remainingEnergy -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_covered,
wrappedList(fee.feeEnergy),
)
}
else -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_reduced,
wrappedList(fee.remainingEnergy),
)
}
}
val prefix = resourceReference(
R.string.send_summary_transaction_description_prefix,
wrappedList(fiatSending),
)
return combinedReference(prefix, stringReference("$COMA_SIGN "), suffix)
}
internal fun formatFooterFiatFee(
amount: Amount?,
isFeeConvertibleToFiat: Boolean,
isFeeApproximate: Boolean,
appCurrency: AppCurrency,
): String {
return if (isFeeConvertibleToFiat) {
amount?.value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
} else {
amount?.value.format {
crypto(
decimals = amount?.decimals ?: 0,
symbol = amount?.currencySymbol.orEmpty(),
).fee(
canBeLower = isFeeApproximate,
)
}
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
@ -286,6 +287,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(
confirmUM = SendConfirmInitialStateTransformer(
isShowTapHelp = isShowTapHelp,
walletName = stringReference(userWallet.name),
).transform(uiState.value.confirmUM),
)
}

View file

@ -7,9 +7,11 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendConfirmInitialStateTransformer(
private val isShowTapHelp: Boolean,
private val walletName: TextReference,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
return ConfirmUM.Content(
walletName = walletName,
isSending = false,
showTapHelp = isShowTapHelp,
sendingFooter = TextReference.EMPTY,

View file

@ -1,13 +1,14 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -16,6 +17,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.utils.formatFooterFiatFee
import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
@ -92,17 +95,18 @@ internal class SendConfirmationNotificationsTransformer(
fiatCurrencySymbol = appCurrency.symbol,
)
}
val fiatFee = formatFiatFee(
val fiatFee = formatFooterFiatFee(
amount = fee.amount,
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
appCurrency = appCurrency,
)
return if (feeUM.isTronToken && fee is Fee.Tron) {
getTokenFeeSendingText(
getTronTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = fiatSending,
fiatSending = stringReference(fiatSending),
)
} else {
resourceReference(
@ -115,57 +119,4 @@ internal class SendConfirmationNotificationsTransformer(
)
}
}
private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String {
return if (isFeeConvertibleToFiat) {
amount?.value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
} else {
amount?.value.format {
crypto(
decimals = amount?.decimals ?: 0,
symbol = amount?.currencySymbol.orEmpty(),
).fee(
canBeLower = isFeeApproximate,
)
}
}
}
private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference {
val suffix = when {
fee.remainingEnergy == 0L -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_including,
wrappedList(fiatFee),
)
}
fee.feeEnergy <= fee.remainingEnergy -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_covered,
wrappedList(fee.feeEnergy),
)
}
else -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_fee_reduced,
wrappedList(fee.remainingEnergy),
)
}
}
val prefix = resourceReference(
R.string.send_summary_transaction_description_prefix,
wrappedList(fiatSending),
)
return combinedReference(prefix, COMMA_SEPARATOR, suffix)
}
companion object {
private val COMMA_SEPARATOR = stringReference(", ")
}
}

View file

@ -1,33 +1,24 @@
package com.tangem.features.send.v2.send.confirm.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
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.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.send.v2.common.ui.SendingText
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
@ -82,38 +73,6 @@ internal fun SendConfirmContent(
}
}
@Composable
private fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) }
val keyboard by keyboardAsState()
// the text should appear when the keyboard is closed
LaunchedEffect(footerText != TextReference.EMPTY, keyboard) {
if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) {
return@LaunchedEffect
}
isVisibleProxy = footerText != TextReference.EMPTY
}
AnimatedVisibility(
visible = isVisibleProxy,
modifier = modifier,
enter = slideInVertically() + fadeIn(),
exit = fadeOut(tween(durationMillis = 300)),
label = "Animate show sending state text",
) {
Text(
text = footerText.resolveAnnotatedReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
)
}
}
private fun LazyListScope.blocks(
uiState: SendUM,
destinationBlockComponent: SendDestinationBlockComponent,

View file

@ -12,7 +12,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.stringReference
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
@ -228,6 +228,7 @@ internal class NFTSendConfirmModel @Inject constructor(
it.copy(
confirmUM = NFTSendConfirmInitialStateTransformer(
isShowTapHelp = isShowTapHelp,
walletName = stringReference(userWallet.name),
).transform(uiState.value.confirmUM),
)
}
@ -357,6 +358,7 @@ internal class NFTSendConfirmModel @Inject constructor(
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
analyticsCategoryName = analyticsCategoryName,
appCurrency = params.appCurrency,
).transform(uiState.value.confirmUM),
)
}
@ -369,15 +371,13 @@ internal class NFTSendConfirmModel @Inject constructor(
transform = { state, route -> state to route },
).onEach { (state, _) ->
val confirmUM = state.confirmUM
val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending
val confirmUMContent = confirmUM as? ConfirmUM.Content
val isReadyToSend = confirmUMContent != null && !confirmUM.isSending
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = resourceReference(
id = R.string.send_summary_title,
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
),
subtitle = null,
title = resourceReference(R.string.nft_send),
subtitle = confirmUMContent?.walletName,
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(

View file

@ -7,12 +7,14 @@ import kotlinx.collections.immutable.persistentListOf
internal class NFTSendConfirmInitialStateTransformer(
private val isShowTapHelp: Boolean,
private val walletName: TextReference,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
return ConfirmUM.Content(
isSending = false,
showTapHelp = isShowTapHelp,
sendingFooter = TextReference.EMPTY,
walletName = walletName,
notifications = persistentListOf(),
)
}

View file

@ -1,12 +1,20 @@
package com.tangem.features.send.v2.sendnft.confirm.model.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.utils.formatFooterFiatFee
import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
@ -19,11 +27,13 @@ internal class NFTSendConfirmationNotificationsTransformer(
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val analyticsCategoryName: String,
private val appCurrency: AppCurrency,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
val feeUM = feeUM as? FeeUM.Content ?: return prevState
return state.copy(
sendingFooter = getSendingFooterText(),
notifications = buildList {
addTooHighNotification(feeUM.feeSelectorUM)
addTooLowNotification(feeUM)
@ -48,6 +58,38 @@ internal class NFTSendConfirmationNotificationsTransformer(
}
}
private fun getSendingFooterText(): TextReference {
val feeUM = feeUM as? FeeUM.Content
val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee ?: return TextReference.EMPTY
val fiatFee = formatFooterFiatFee(
amount = fee.amount,
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
isFeeApproximate = feeUM.isFeeApproximate,
appCurrency = appCurrency,
)
return if (feeUM.isTronToken && fee is Fee.Tron) {
getTronTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = resourceReference(R.string.common_nft),
)
} else {
resourceReference(
id = if (feeUM.isFeeConvertibleToFiat) {
R.string.send_summary_transaction_description
} else {
R.string.send_summary_transaction_description_no_fiat_fee
},
formatArgs = wrappedList(
resourceReference(R.string.common_nft),
fiatFee,
),
)
}
}
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
if (feeSelectorUM !is FeeSelectorUM.Content) return

View file

@ -11,13 +11,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
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.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.common.ui.SendingText
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
@ -66,6 +69,8 @@ internal fun NFTSendConfirmContent(
)
}
}
SpacerHMax()
SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.send.v2.sendnft.ui.state
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM