Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-20 16:50:40 +05:00
parent f9feacf074
commit 07f63b47e4
15 changed files with 245 additions and 32 deletions

View file

@ -70,5 +70,9 @@
{ {
"name": "SWAP_REDESIGN_ENABLED", "name": "SWAP_REDESIGN_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "SEND_REDESIGN_ENABLED",
"version": "undefined"
} }
] ]

View file

@ -856,8 +856,12 @@
<string name="send_recipient">Recipient</string> <string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string> <string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_address_footer">Ensure the receiving wallet address is on the %s network to avoid losing your tokens</string> <string name="send_recipient_address_footer">Ensure the receiving wallet address is on the %s network to avoid losing your tokens</string>
<string name="send_recipient_address_footer_highlighted_part">send to %s</string>
<string name="send_recipient_address_footer_v2">Ensure you %s network address, as errors may result in lost transfers</string>
<string name="send_recipient_label">Send to</string> <string name="send_recipient_label">Send to</string>
<string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds</string> <string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds</string>
<string name="send_recipient_memo_footer_v2">Memo / Destination Tag is a code separating transactions to a shared recipient in a crypto network.</string>
<string name="send_recipient_memo_footer_v2_highlighted">Caution: Missing a memo may lead to fund loss.</string>
<string name="send_recipient_wallets_title">My wallets</string> <string name="send_recipient_wallets_title">My wallets</string>
<string name="send_satoshi_per_byte_text">A way of measuring Bitcoin transaction fees. It indicates the number of the smallest Bitcoin unit (Satoshi) for each virtual byte in a transaction. The higher the number, the faster the transaction will be processed by miners.</string> <string name="send_satoshi_per_byte_text">A way of measuring Bitcoin transaction fees. It indicates the number of the smallest Bitcoin unit (Satoshi) for each virtual byte in a transaction. The higher the number, the faster the transaction will be processed by miners.</string>
<string name="send_satoshi_per_byte_title">Satoshi / vByte</string> <string name="send_satoshi_per_byte_title">Satoshi / vByte</string>
@ -885,7 +889,7 @@
<string name="settings_forget_wallet">Forget wallet</string> <string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string> <string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string> <string name="settings_wallet_name_title">Name</string>
<string name="stake_token_description">Join staking with this token</string> <string name="stake_token_description">Put your token to work</string>
<string name="staking_amount_requirement_error">The amount to stake must be at least %s</string> <string name="staking_amount_requirement_error">The amount to stake must be at least %s</string>
<string name="staking_amount_tron_integer_error">Staking amount will be rounded to %1$s TRX due to network rules.</string> <string name="staking_amount_tron_integer_error">Staking amount will be rounded to %1$s TRX due to network rules.</string>
<string name="staking_amount_tron_integer_error_unstaking">Unstaking amount will be rounded to %1$s TRX due to network rules.</string> <string name="staking_amount_tron_integer_error_unstaking">Unstaking amount will be rounded to %1$s TRX due to network rules.</string>

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
/** /**
@ -32,7 +32,7 @@ fun FooterContainer(
AnimatedVisibility(visible = footer != null) { AnimatedVisibility(visible = footer != null) {
val footerWrapped = remember(this) { requireNotNull(footer) } val footerWrapped = remember(this) { requireNotNull(footer) }
Text( Text(
text = footerWrapped.resolveReference(), text = footerWrapped.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
modifier = Modifier modifier = Modifier

View file

@ -2,6 +2,10 @@ package com.tangem.core.ui.components.inputrow
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -16,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.CrossIcon import com.tangem.core.ui.components.inputrow.inner.CrossIcon
@ -25,24 +30,29 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
/** /**
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) * * [Design Reference](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4)
* *
* @param title title reference * @param title The title text reference to display above the input field
* @param value recipient address * @param value The current recipient address value to display in the input field
* @param placeholder placeholder * @param placeholder The placeholder text reference to show when the input is empty
* @param onValueChange callback for value change * @param onValueChange Callback invoked when the input value changes, receives the new string value
* @param onPasteClick callback for paste * @param onPasteClick Callback invoked when the paste button is clicked, receives the pasted string
* @param modifier composable modifier * @param onQrCodeClick Callback invoked when the QR code scan button is clicked
* @param singleLine is single line text * @param modifier Optional modifier to apply to the composable
* @param error error text * @param singleLine Whether the text field should be constrained to a single line (default: false)
* @param isError is error flag * @param error Optional error text reference to display when validation fails
* @param showDivider show divider * @param isError Whether the input is in an error state, affects styling and title color
* @param showDivider Whether to show a divider below the component (default: false)
* @param isLoading Whether to show a loading indicator instead of the identicon (default: false)
* @param isValuePasted Whether the current value was pasted, affects visual feedback (default: false)
* *
* @see InputRowRecipientDefault for readonly version * @see InputRowRecipientDefault for a read-only version of this component
*/ */
@Suppress("LongParameterList", "LongMethod")
@Composable @Composable
fun InputRowRecipient( fun InputRowRecipient(
title: TextReference, title: TextReference,
@ -50,6 +60,8 @@ fun InputRowRecipient(
placeholder: TextReference, placeholder: TextReference,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit, onPasteClick: (String) -> Unit,
onQrCodeClick: () -> Unit,
isRedesignEnabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
singleLine: Boolean = false, singleLine: Boolean = false,
error: TextReference? = null, error: TextReference? = null,
@ -109,18 +121,55 @@ fun InputRowRecipient(
.padding(start = TangemTheme.dimens.spacing8), .padding(start = TangemTheme.dimens.spacing8),
) )
} }
PasteButton( Row(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier modifier = Modifier
.align(CenterEnd) .align(CenterEnd),
.padding(start = TangemTheme.dimens.spacing8), ) {
) if (isRedesignEnabled) {
QrButton(
visible = !singleLine,
onQrCodeClick = onQrCodeClick,
)
}
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
backgroundColorEnabled = if (isRedesignEnabled) {
TangemTheme.colors.button.secondary
} else {
TangemTheme.colors.button.primary
},
textColor = if (isRedesignEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
)
}
} }
} }
} }
} }
@Composable
private fun QrButton(visible: Boolean, onQrCodeClick: () -> Unit, modifier: Modifier = Modifier) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut(animationSpec = tween(DEFAULT_ANIMATION_DURATION)),
modifier = modifier,
) {
TangemIconButton(
iconRes = R.drawable.ic_scan_16,
onClick = onQrCodeClick,
background = TangemTheme.colors.button.secondary,
iconTint = TangemTheme.colors.text.primary1,
)
}
}
@Composable @Composable
private fun RowScope.InputIcon(isLoading: Boolean, value: String) { private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
var isLoadingProxy by remember { mutableStateOf(isLoading) } var isLoadingProxy by remember { mutableStateOf(isLoading) }
@ -172,7 +221,9 @@ private fun InputRowRecipientPreview(
showDivider = true, showDivider = true,
onValueChange = {}, onValueChange = {},
onPasteClick = {}, onPasteClick = {},
onQrCodeClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary), modifier = Modifier.background(TangemTheme.colors.background.primary),
isRedesignEnabled = false,
) )
} }
} }

View file

@ -14,6 +14,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@ -34,14 +35,21 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
* @param modifier composable modifier * @param modifier composable modifier
*/ */
@Composable @Composable
fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { fun PasteButton(
isPasteButtonVisible: Boolean,
onClick: (String) -> Unit,
modifier: Modifier = Modifier,
backgroundColorEnabled: Color = TangemTheme.colors.button.primary,
backgroundColorDisabled: Color = TangemTheme.colors.button.secondary,
textColor: Color = TangemTheme.colors.text.primary2,
) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty()
val color = if (isPasteEnabled) { val color = if (isPasteEnabled) {
TangemTheme.colors.button.primary backgroundColorEnabled
} else { } else {
TangemTheme.colors.button.secondary backgroundColorDisabled
} }
AnimatedVisibility( AnimatedVisibility(
visible = isPasteButtonVisible, visible = isPasteButtonVisible,
@ -53,7 +61,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
Text( Text(
text = stringResourceSafe(R.string.common_paste), text = stringResourceSafe(R.string.common_paste),
style = TangemTheme.typography.caption1, style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary2, color = textColor,
modifier = Modifier modifier = Modifier
.background( .background(
color = color, color = color,

View file

@ -60,4 +60,40 @@ fun AnnotatedString.Builder.appendSpace() = append(" ")
fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) { fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) {
append(text) append(text)
}
/**
* Appends text from a template string to the AnnotatedString.Builder, replacing a placeholder (default "%s")
* with custom styled content provided by a lambda. The lambda allows you to insert styled or complex content
* (e.g., colored, bold, or annotated text) at the placeholder position.
*
* If the placeholder is not found in the template, the function does nothing.
*
* Example usage:
* builder.appendWithStyledPlaceholder(
* template = "Ensure you %s network address, as errors may result in lost transfers"
* ) {
* withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)) {
* append("Ethereum")
* }
* }
*
* @param template The template string containing the placeholder to be replaced.
* @param placeholder The placeholder string to be replaced by styled content. Default is "%s".
* @param styledContent Lambda to build the styled content to insert at the placeholder position.
*/
fun AnnotatedString.Builder.appendWithStyledPlaceholder(
template: String,
placeholder: String = "%s",
styledContent: AnnotatedString.Builder.() -> Unit,
) {
val index = template.indexOf(placeholder)
if (index < 0) return
// Append before placeholder
if (index > 0) append(template.substring(0, index))
// Append styled content
styledContent()
// Append after placeholder
val afterIndex = index + placeholder.length
if (afterIndex < template.length) append(template.substring(afterIndex))
} }

View file

@ -0,0 +1,10 @@
<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>

View file

@ -1,3 +1,6 @@
package com.tangem.features.send.v2.api package com.tangem.features.send.v2.api
interface SendFeatureToggles interface SendFeatureToggles {
val isSendRedesignEnabled: Boolean
}

View file

@ -4,5 +4,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.SendFeatureToggles
internal class DefaultSendFeatureToggles( internal class DefaultSendFeatureToggles(
@Suppress("UnusedPrivateMember") private val featureToggles: FeatureTogglesManager, private val featureToggles: FeatureTogglesManager,
) : SendFeatureToggles ) : SendFeatureToggles {
override val isSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
}

View file

@ -37,6 +37,7 @@ import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.SendConfirmAlertFactory
@ -87,6 +88,7 @@ internal class SendModel @Inject constructor(
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase, private val getFeeUseCase: GetFeeUseCase,
private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger, private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendComponentCallback { ) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require() private val params: SendComponent.Params = paramsContainer.require()
@ -357,6 +359,7 @@ internal class SendModel @Inject constructor(
amountUM = AmountState.Empty(), amountUM = AmountState.Empty(),
destinationUM = SendDestinationInitialStateTransformer( destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).transform(DestinationUM.Empty()), ).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(), feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty, confirmUM = ConfirmUM.Empty,

View file

@ -23,6 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.isLocked import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
@ -65,6 +66,7 @@ internal class SendDestinationModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendDestinationClickIntents { ) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require() private val params: SendDestinationComponentParams = paramsContainer.require()
@ -90,6 +92,7 @@ internal class SendDestinationModel @Inject constructor(
_uiState.update( _uiState.update(
SendDestinationInitialStateTransformer( SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isInitialized = true, isInitialized = true,
), ),
) )
@ -297,6 +300,7 @@ internal class SendDestinationModel @Inject constructor(
flow2 = params.currentRoute, flow2 = params.currentRoute,
transform = { state, route -> state to route }, transform = { state, route -> state to route },
).onEach { (state, route) -> ).onEach { (state, route) ->
val isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled
params.callback.onNavigationResult( params.callback.onNavigationResult(
NavigationUM.Content( NavigationUM.Content(
title = params.title, title = params.title,
@ -319,8 +323,16 @@ internal class SendDestinationModel @Inject constructor(
} }
params.onBackClick() params.onBackClick()
}, },
additionalIconRes = R.drawable.ic_qrcode_scan_24, additionalIconRes = if (isRedesignEnabled) {
additionalIconClick = ::onQrCodeScanClick, null
} else {
R.drawable.ic_qrcode_scan_24
},
additionalIconClick = if (isRedesignEnabled) {
null
} else {
::onQrCodeScanClick
},
primaryButton = ButtonsUM.PrimaryButtonUM( primaryButton = ButtonsUM.PrimaryButtonUM(
text = if (route.isEditMode) { text = if (route.isEditMode) {
resourceReference(R.string.common_continue) resourceReference(R.string.common_continue)

View file

@ -13,6 +13,7 @@ import com.tangem.utils.transformer.Transformer
internal class SendDestinationInitialStateTransformer( internal class SendDestinationInitialStateTransformer(
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val isRedesignEnabled: Boolean,
val isInitialized: Boolean = false, val isInitialized: Boolean = false,
) : Transformer<DestinationUM> { ) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM { override fun transform(prevState: DestinationUM): DestinationUM {
@ -54,6 +55,7 @@ internal class SendDestinationInitialStateTransformer(
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrency.network.name, networkName = cryptoCurrency.network.name,
isValidating = false, isValidating = false,
isRedesignEnabled = isRedesignEnabled,
) )
} }
} }

View file

@ -16,6 +16,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.components.inputrow.InputRowRecipient
@ -60,10 +64,13 @@ internal fun SendDestinationContent(
isError = isError, isError = isError,
isValidating = isValidating, isValidating = isValidating,
onAddressChange = clickIntents::onRecipientAddressValueChange, onAddressChange = clickIntents::onRecipientAddressValueChange,
onQrCodeClick = clickIntents::onQrCodeScanClick,
isRedesignEnabled = state.isRedesignEnabled,
) )
memoField( memoField(
memoField = memoField, memoField = memoField,
onMemoChange = clickIntents::onRecipientMemoValueChange, onMemoChange = clickIntents::onRecipientMemoValueChange,
isRedesignEnabled = state.isRedesignEnabled,
) )
listHeaderItem( listHeaderItem(
titleRes = R.string.send_recipient_wallets_title, titleRes = R.string.send_recipient_wallets_title,
@ -100,16 +107,23 @@ internal fun SendDestinationContent(
} }
} }
@Suppress("LongParameterList")
private fun LazyListScope.addressItem( private fun LazyListScope.addressItem(
address: DestinationTextFieldUM.RecipientAddress, address: DestinationTextFieldUM.RecipientAddress,
networkName: String, networkName: String,
isError: Boolean, isError: Boolean,
isValidating: Boolean, isValidating: Boolean,
onAddressChange: (String, EnterAddressSource) -> Unit, onAddressChange: (String, EnterAddressSource) -> Unit,
onQrCodeClick: () -> Unit,
isRedesignEnabled: Boolean,
) { ) {
item(key = ADDRESS_FIELD_KEY) { item(key = ADDRESS_FIELD_KEY) {
FooterContainer( FooterContainer(
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)), footer = if (isRedesignEnabled) {
buildHighlightedFooterText(networkName)
} else {
resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName))
},
) { ) {
InputRowRecipient( InputRowRecipient(
value = address.value, value = address.value,
@ -117,6 +131,8 @@ private fun LazyListScope.addressItem(
placeholder = address.placeholder, placeholder = address.placeholder,
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) },
onQrCodeClick = onQrCodeClick,
isRedesignEnabled = isRedesignEnabled,
isError = isError, isError = isError,
isLoading = isValidating, isLoading = isValidating,
error = address.error, error = address.error,
@ -131,9 +147,37 @@ private fun LazyListScope.addressItem(
} }
} }
@Composable
private fun buildHighlightedFooterText(networkName: String): TextReference {
return annotatedReference(
buildAnnotatedString {
val styledText = stringResourceSafe(
R.string.send_recipient_address_footer_highlighted_part,
networkName,
)
val styledColor = TangemTheme.colors.text.secondary
appendWithStyledPlaceholder(
template = stringResourceSafe(
R.string.send_recipient_address_footer_v2,
networkName,
),
placeholder = networkName,
) {
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = styledText,
color = styledColor,
)
}
}
},
)
}
private fun LazyListScope.memoField( private fun LazyListScope.memoField(
memoField: DestinationTextFieldUM.RecipientMemo?, memoField: DestinationTextFieldUM.RecipientMemo?,
onMemoChange: (String, Boolean) -> Unit, onMemoChange: (String, Boolean) -> Unit,
isRedesignEnabled: Boolean,
) { ) {
if (memoField != null) { if (memoField != null) {
item(key = MEMO_FIELD_KEY) { item(key = MEMO_FIELD_KEY) {
@ -142,7 +186,24 @@ private fun LazyListScope.memoField(
value = memoField.value, value = memoField.value,
label = memoField.label, label = memoField.label,
placeholder = placeholder, placeholder = placeholder,
footer = resourceReference(R.string.send_recipient_memo_footer), footer = if (isRedesignEnabled) {
annotatedReference(
buildAnnotatedString {
append(stringResourceSafe(R.string.send_recipient_memo_footer_v2))
append("\n")
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = stringResourceSafe(
R.string.send_recipient_memo_footer_v2_highlighted,
),
color = TangemTheme.colors.text.secondary,
)
}
},
)
} else {
resourceReference(R.string.send_recipient_memo_footer)
},
onValueChange = { onMemoChange(it, false) }, onValueChange = { onMemoChange(it, false) },
onPasteClick = { onMemoChange(it, true) }, onPasteClick = { onMemoChange(it, true) },
modifier = Modifier.padding(top = 20.dp), modifier = Modifier.padding(top = 20.dp),
@ -151,6 +212,7 @@ private fun LazyListScope.memoField(
error = memoField.error, error = memoField.error,
isReadOnly = !memoField.isEnabled, isReadOnly = !memoField.isEnabled,
isValuePasted = memoField.isValuePasted, isValuePasted = memoField.isValuePasted,
isRedesignEnabled = isRedesignEnabled,
) )
} }
} }

View file

@ -20,6 +20,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.containers.FooterContainer
@Suppress("LongMethod", "LongParameterList")
@Composable @Composable
internal fun TextFieldWithPaste( internal fun TextFieldWithPaste(
value: String, value: String,
@ -27,6 +28,7 @@ internal fun TextFieldWithPaste(
label: TextReference, label: TextReference,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit, onPasteClick: (String) -> Unit,
isRedesignEnabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
footer: TextReference? = null, footer: TextReference? = null,
labelStyle: TextStyle = TangemTheme.typography.body2, labelStyle: TextStyle = TangemTheme.typography.body2,
@ -96,6 +98,18 @@ internal fun TextFieldWithPaste(
PasteButton( PasteButton(
isPasteButtonVisible = value.isBlank(), isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick, onClick = onPasteClick,
backgroundColorEnabled = if (isRedesignEnabled) {
TangemTheme.colors.button.secondary
} else {
TangemTheme.colors.button.primary
},
textColor = if (isRedesignEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
) )
} }
} }

View file

@ -17,6 +17,7 @@ internal sealed class DestinationUM {
val networkName: String, val networkName: String,
val isValidating: Boolean = false, val isValidating: Boolean = false,
val isInitialized: Boolean = false, val isInitialized: Boolean = false,
val isRedesignEnabled: Boolean = false,
) : DestinationUM() ) : DestinationUM()
data class Empty( data class Empty(