Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-24 14:43:38 +05:00
parent 22ad2ddc67
commit 281b5ec956
13 changed files with 371 additions and 20 deletions

View file

@ -0,0 +1,180 @@
package com.tangem.common.ui.amountScreen.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.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.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
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.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun AmountBlockV2(
amountState: AmountState,
currencyIconState: CurrencyIconState,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (amountState !is AmountState.Data) return
val amount = amountState.amountTextField
val cryptoAmount = amount.cryptoAmount.value?.format {
crypto(
symbol = "",
decimals = amount.cryptoAmount.decimals,
)
}.orEmpty()
val fiatAmount = amount.fiatAmount.value.format {
fiat(
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencyCode = amountState.appCurrency.code,
)
}
val (firstAmount, secondAmount) = if (amount.isFiatValue) {
fiatAmount to cryptoAmount
} else {
cryptoAmount to fiatAmount
}
val title = TextReference.Str(
stringResourceSafe(
R.string.send_from_wallet_name,
amountState.title
.resolveReference(),
),
)
val currencyTitle = amount.cryptoAmount.currencySymbol
AmountBlockV2(
title = title,
balance = amountState.availableBalance,
currencyTitle = currencyTitle,
currencyIconState = currencyIconState,
firstAmount = firstAmount,
secondAmount = secondAmount,
isClickDisabled = isClickDisabled,
isEditingDisabled = isEditingDisabled,
onClick = onClick,
modifier = modifier,
)
}
@Suppress("LongParameterList")
@Composable
internal fun AmountBlockV2(
title: TextReference,
balance: TextReference,
currencyTitle: String,
currencyIconState: CurrencyIconState,
firstAmount: String,
secondAmount: String,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing16),
) {
Row {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Spacer(modifier = modifier.weight(1f))
Text(
text = balance.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 8.dp),
) {
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 6.dp),
) {
CurrencyIcon(
state = currencyIconState,
modifier = Modifier.padding(horizontal = 14.dp),
)
Text(
text = currencyTitle,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AmountBlockPreview(@PreviewParameter(AmountBlockV2PreviewProvider::class) value: AmountState) {
TangemThemePreview {
AmountBlockV2(
amountState = value,
currencyIconState = CurrencyIconState.Empty(),
isClickDisabled = false,
isEditingDisabled = false,
onClick = {},
)
}
}
private class AmountBlockV2PreviewProvider : PreviewParameterProvider<AmountState> {
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountState,
)
}
// endregion

View file

@ -822,6 +822,7 @@
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_wallet_android">From</string>
<string name="send_from_wallet_name">From %s</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction.</string>
<string name="send_gas_price">Gas price</string>
@ -829,6 +830,7 @@
<string name="send_max_amount">Max</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_max_fee">Fee up to</string>
<string name="send_memo">Memo: %s</string>
<string name="send_memo_destination_tag_error">Invalid Memo</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_nonce">Nonce</string>
@ -879,6 +881,7 @@
<string name="send_summary_transaction_description_suffix_fee_covered">network fee will be covered by using %1$s energy</string>
<string name="send_summary_transaction_description_suffix_fee_reduced">network fee will be reduced by spending %1$s energy</string>
<string name="send_summary_transaction_description_suffix_including">including a network fee of %1$s</string>
<string name="send_to_address">To address</string>
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_tron_account_activation_error">%1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account.</string>
<string name="send_validation_amount_exceeds_balance">Amount exceeds balance</string>
@ -888,6 +891,7 @@
<string name="send_validation_invalid_fee">Fee exceeds balance</string>
<string name="send_validation_invalid_total">Total amount exceeds balance</string>
<string name="send_with_swap_notification_text">Send any token, and well convert it on the way. Your recipient gets exactly what they need—seamlessly.</string>
<string name="send_with_swap_recipient_amount_text">Will be sent a recipient</string>
<string name="send_with_swap_title">Send with swap</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card or ring you want to set up.</string>

View file

@ -1,10 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="17dp"
android:viewportWidth="16"
android:viewportHeight="17">
<path
android:pathData="M6.666,2.391H4C2.895,2.391 2,3.287 2,4.391V7.391L3.333,7.391V4.391C3.333,4.023 3.631,3.725 4,3.725H6.666V2.391ZM2,10.391V12.391C2,13.496 2.895,14.391 4,14.391H6.666V13.058H4C3.631,13.058 3.333,12.759 3.333,12.391V10.391H12.666V12.391C12.666,12.759 12.368,13.058 12,13.058H9.333V14.391H12C13.104,14.391 14,13.496 14,12.391V10.391H14.666V9.058H1.333V10.391H2ZM14,4.391V7.391H12.666V4.391C12.666,4.023 12.368,3.725 12,3.725H9.333V2.391H12C13.104,2.391 14,3.287 14,4.391Z"
android:fillColor="#1E1E1E"
android:fillType="evenOdd"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
<path android:fillColor="#1E1E1E" android:fillType="evenOdd" android:pathData="M10,3H6C4.343,3 3,4.343 3,6V10.5H5V6C5,5.448 5.448,5 6,5H10V3ZM3,15V18C3,19.657 4.343,21 6,21H10V19H6C5.448,19 5,18.552 5,18V15H19V18C19,18.552 18.552,19 18,19H14V21H18C19.657,21 21,19.657 21,18V15H22V13H2V15H3ZM21,6V10.5H19V6C19,5.448 18.552,5 18,5H14V3H18C19.657,3 21,4.343 21,6Z"/>
</vector>

View file

@ -206,6 +206,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
childStack = childStack,
)
},
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
),
)
} else {
@ -225,6 +226,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
return if (sendAmount != null && destinationAddress != null &&
feeCryptoCurrencyStatus != null && cryptoCurrencyStatus != null
) {
// TODO Apply new component [REDACTED_TASK_KEY]
SendFeeComponent(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(

View file

@ -65,6 +65,7 @@ internal class SendConfirmComponent(
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
predefinedValues = params.predefinedValues,
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,

View file

@ -454,6 +454,7 @@ internal class SendConfirmModel @Inject constructor(
}
}
@Suppress("LongMethod")
private fun configConfirmNavigation() {
combine(
flow = uiState,
@ -470,7 +471,11 @@ internal class SendConfirmModel @Inject constructor(
id = R.string.send_summary_title,
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
),
subtitle = amountUM?.title,
subtitle = if (uiState.value.isRedesignEnabled) {
null
} else {
amountUM?.title
},
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(

View file

@ -98,8 +98,13 @@ private fun LazyListScope.blocks(
modifier = Modifier.padding(vertical = 12.dp),
)
}
destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
if (uiState.isRedesignEnabled) {
amountBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
} else {
destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
}
feeBlockComponent.Content(modifier = Modifier)
}
}

View file

@ -364,5 +364,6 @@ internal class SendModel @Inject constructor(
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
)
}

View file

@ -12,4 +12,5 @@ internal data class SendUM(
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
val isRedesignEnabled: Boolean,
)

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
@ -37,11 +38,24 @@ internal class SendAmountBlockComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
if (params.isRedesignEnabled) {
val amountState = state as? AmountState.Data ?: return
AmountBlockV2(
amountState = state,
currencyIconState = amountState.tokenIconState,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
modifier = modifier,
)
} else {
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
}
}
}

View file

@ -18,6 +18,7 @@ internal sealed class SendAmountComponentParams {
abstract val appCurrency: AppCurrency
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
abstract val predefinedValues: PredefinedValues
abstract val isRedesignEnabled: Boolean
data class AmountParams(
override val state: AmountState,
@ -26,6 +27,7 @@ internal sealed class SendAmountComponentParams {
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
val callback: ModelCallback,
val currentRoute: Flow<CommonSendRoute.Amount>,
val isBalanceHidingFlow: StateFlow<Boolean>,
@ -40,6 +42,7 @@ internal sealed class SendAmountComponentParams {
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()
}

View file

@ -40,6 +40,7 @@ internal class SendDestinationBlockComponent(
destinationUM = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
isRedesignEnabled = (params.state as? DestinationUM.Content)?.isRedesignEnabled ?: false,
onClick = onClick,
)
}

View file

@ -1,26 +1,38 @@
package com.tangem.features.send.v2.subcomponents.destination.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
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.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun DestinationBlock(
destinationUM: DestinationUM,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
isRedesignEnabled: Boolean,
onClick: () -> Unit,
) {
if (destinationUM !is DestinationUM.Content) return
@ -33,8 +45,15 @@ internal fun DestinationBlock(
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12),
) {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
if (isRedesignEnabled) {
AddressWithMemoBlock(
address = destinationUM.addressTextField,
memo = destinationUM.memoTextField,
)
} else {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
}
}
}
@ -84,4 +103,124 @@ private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) {
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
@Composable
private fun AddressWithMemoBlock(
address: DestinationTextFieldUM.RecipientAddress,
memo: DestinationTextFieldUM.RecipientMemo?,
) {
Text(
text = stringResourceSafe(R.string.send_to_address),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
Text(
modifier = Modifier.weight(1f),
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
IdentIcon(
address = address.value,
modifier = Modifier
.size(TangemTheme.dimens.size36)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.background(TangemTheme.colors.background.tertiary),
)
}
if (memo != null && memo.value.isNotBlank()) {
Text(
text = stringResourceSafe(R.string.send_memo, memo.value),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DestinationBlockPreview(
@PreviewParameter(DestinationBlockPreviewProvider::class) state: DestinationUM.Content,
) {
TangemThemePreview {
DestinationBlock(
destinationUM = state,
isClickDisabled = false,
isEditingDisabled = false,
isRedesignEnabled = state.isRedesignEnabled,
onClick = {},
)
}
}
private class DestinationBlockPreviewProvider : PreviewParameterProvider<DestinationUM.Content> {
override val values: Sequence<DestinationUM.Content>
get() = sequenceOf(
DestinationUM.Content(
isPrimaryButtonEnabled = true,
addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter address"),
label = TextReference.Str("Recipient Address"),
isError = false,
error = null,
isValuePasted = false,
),
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "Test memo for transaction",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter memo (optional)"),
label = TextReference.Str("Memo"),
isError = false,
error = null,
disabledText = TextReference.Str("Memo disabled"),
isEnabled = true,
isValuePasted = false,
),
recent = emptyList<DestinationRecipientListUM>().toImmutableList(),
wallets = emptyList<DestinationRecipientListUM>().toImmutableList(),
networkName = "Ethereum",
isValidating = false,
isInitialized = true,
isRedesignEnabled = false,
),
DestinationUM.Content(
isPrimaryButtonEnabled = true,
addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter address"),
label = TextReference.Str("Recipient Address"),
isError = false,
error = null,
isValuePasted = false,
),
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "Test memo for transaction",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter memo (optional)"),
label = TextReference.Str("Memo"),
isError = false,
error = null,
disabledText = TextReference.Str("Memo disabled"),
isEnabled = true,
isValuePasted = false,
),
recent = emptyList<DestinationRecipientListUM>().toImmutableList(),
wallets = emptyList<DestinationRecipientListUM>().toImmutableList(),
networkName = "Ethereum",
isValidating = false,
isInitialized = true,
isRedesignEnabled = true,
),
)
}