Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-30 12:50:32 +03:00
parent 154308b973
commit e51453c99b
15 changed files with 173 additions and 68 deletions

View file

@ -65,6 +65,10 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS
else -> Network.NameResolvingType.NONE
},
)
return factory.createCoin(network = network)
@ -95,6 +99,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",

View file

@ -64,6 +64,10 @@
<string name="auth_info_add_wallet_title">Add Wallet</string>
<string name="auth_info_subtitle">Select a wallet to log in</string>
<string name="auth_info_title">Welcome back!</string>
<plurals name="auth_wallet_hardware_description">
<item quantity="one">%d card</item>
<item quantity="other">%d cards</item>
</plurals>
<string name="auth_wallet_mobile_description">Phone Wallet</string>
<string name="backup_complete_description">You successfully backed up your wallet.</string>
<string name="backup_complete_seed_description">These words cant be recovered if lost. Make sure to keep it somewhere secure.</string>
@ -866,6 +870,7 @@
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_enter_address_field_ens">Enter ENS name or address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_error_dust_amount_format">Minimum amount is %s</string>
<string name="send_error_dust_change_format">Minimum change is %s</string>
@ -947,11 +952,15 @@
<string name="send_validation_invalid_amount">Invalid amount</string>
<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_change_token_alert_message">Are you sure you want to change the token? After changing, previous data will be reset.</string>
<string name="send_with_swap_change_token_alert_title">Changing token</string>
<string name="send_with_swap_confirm_title">Swap and send</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_success_title">Recipient will receive</string>
<string name="send_with_swap_recipient_amount_text">Will be sent a recipient</string>
<string name="send_with_swap_recipient_amount_title">Amount to receive</string>
<string name="send_with_swap_remove_convert_alert_message">Are you sure you want to cancel the conversion? After changing, previous data will be reset.</string>
<string name="send_with_swap_remove_convert_alert_title">Confirm cancelation</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

@ -10,6 +10,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterEnd
@ -19,6 +20,7 @@ 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.core.ui.R
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.fields.SimpleTextField
@ -69,6 +71,7 @@ fun InputRowRecipient(
showDivider: Boolean = false,
isLoading: Boolean = false,
isValuePasted: Boolean = false,
resolvedAddress: String? = null,
) {
val (titleText, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
@ -144,11 +147,15 @@ fun InputRowRecipient(
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
}
}
ResolvedAddressRow(
isLoading = isLoading,
resolvedAddress = resolvedAddress,
)
}
}
}
@ -203,6 +210,38 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
}
}
@Composable
private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) {
AnimatedContent(
targetState = if (resolvedAddress.isNullOrBlank() || isLoading) {
ResolvedState.Hide
} else {
ResolvedState.Show(resolvedAddress)
},
label = "Resolved Address",
) { state ->
if (state is ResolvedState.Show) {
Column {
HorizontalDivider(
thickness = 0.5.dp,
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp),
color = TangemTheme.colors.stroke.primary,
)
Text(
text = state.address,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
private sealed interface ResolvedState {
data object Hide : ResolvedState
data class Show(val address: String) : ResolvedState
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -224,6 +263,7 @@ private fun InputRowRecipientPreview(
onQrCodeClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
isRedesignEnabled = false,
resolvedAddress = value.resolvedAddress,
)
}
}
@ -232,6 +272,7 @@ private data class InputRowRecipientPreviewData(
val value: String,
val isError: Boolean,
val isLoading: Boolean = false,
val resolvedAddress: String? = null,
)
private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<InputRowRecipientPreviewData> {
@ -250,6 +291,12 @@ private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<In
isLoading = true,
isError = true,
),
InputRowRecipientPreviewData(
value = "vitalik.eth",
isLoading = false,
isError = true,
resolvedAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470",
),
)
}
//endregion

View file

@ -111,6 +111,7 @@ class NetworkFactory @Inject constructor(
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = canHandleTokens,
transactionExtrasType = blockchain.getSupportedTransactionExtras(),
nameResolvingType = blockchain.getNameResolvingType(),
)
}
.getOrNull()
@ -328,6 +329,13 @@ class NetworkFactory @Inject constructor(
}
}
private fun Blockchain.getNameResolvingType(): Network.NameResolvingType {
return when (this) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS
else -> Network.NameResolvingType.NONE
}
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun createNetworkStandardType(blockchain: Blockchain) = getNetworkStandardType(blockchain)

View file

@ -149,7 +149,9 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
// TODO: in the future, consider optimizing this part
.chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time
.map {
async(dispatchers.io) { stakeKitApi.getMultipleYieldBalances(it).bind() }
async(dispatchers.io) {
stakeKitApi.getMultipleYieldBalances(it).bind()
}
}
.awaitAll()
.flatten()

View file

@ -20,6 +20,7 @@ import kotlinx.serialization.Serializable
* currency (for those blockchains that have FeeResource instead of a standard type of fee)
* @property canHandleTokens indicates whether the network can handle tokens
* @property transactionExtrasType the type of extras supported for sending a transaction
* @property nameResolvingType the type of on-chain name resolution supported by the network (e.g., ENS, SNS etc)
*/
@Serializable
data class Network(
@ -33,6 +34,7 @@ data class Network(
val hasFiatFeeRate: Boolean,
val canHandleTokens: Boolean,
val transactionExtrasType: TransactionExtrasType,
val nameResolvingType: NameResolvingType,
) {
/** Raw ID */
@ -161,4 +163,16 @@ data class Network(
-> true
}
}
/**
* Represents the type of on-chain name resolution supported by the network.
*/
enum class NameResolvingType {
/** No name resolution supported */
NONE,
/** Ethereum Name Service (ENS) */
ENS,
}
}

View file

@ -24,6 +24,7 @@ internal object MockNetworks {
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
val network2 = Network(
@ -37,6 +38,7 @@ internal object MockNetworks {
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
val network3 = Network(
@ -50,6 +52,7 @@ internal object MockNetworks {
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
val verifiedNetworksStatuses: NonEmptySet<NetworkStatus>

View file

@ -65,6 +65,7 @@ internal class PreviewCustomTokenSelectorComponent(
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "Network $index",
type = "N$index",

View file

@ -163,6 +163,7 @@ internal class PreviewManageTokensComponent(
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "NETWORK$networkIndex",
type = "N$networkIndex",

View file

@ -104,6 +104,7 @@ internal class PreviewOnboardingManageTokensComponent(
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "NETWORK$networkIndex",
type = "N$networkIndex",

View file

@ -22,6 +22,10 @@ internal class SendDestinationInitialStateTransformer(
Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo
Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field
}
val placeholder = when (cryptoCurrency.network.nameResolvingType) {
Network.NameResolvingType.NONE -> resourceReference(R.string.send_enter_address_field)
Network.NameResolvingType.ENS -> resourceReference(R.string.send_enter_address_field_ens)
}
return DestinationUM.Content(
isPrimaryButtonEnabled = false,
isInitialized = isInitialized,
@ -32,7 +36,7 @@ internal class SendDestinationInitialStateTransformer(
keyboardType = KeyboardType.Text,
),
error = null,
placeholder = resourceReference(R.string.send_enter_address_field),
placeholder = placeholder,
label = resourceReference(R.string.send_recipient),
isValuePasted = false,
),

View file

@ -76,11 +76,21 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) {
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.background(TangemTheme.colors.background.tertiary),
)
Text(
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
val blockchainAddress = address.blockchainAddress
if (!blockchainAddress.isNullOrBlank()) {
Text(
text = blockchainAddress,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
@ -120,12 +130,21 @@ private fun AddressWithMemoBlock(
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,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
val blockchainAddress = address.blockchainAddress
if (!blockchainAddress.isNullOrBlank()) {
Text(
text = blockchainAddress,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
IdentIcon(
address = address.value,
modifier = Modifier
@ -134,6 +153,7 @@ private fun AddressWithMemoBlock(
.background(TangemTheme.colors.background.tertiary),
)
}
if (memo != null && memo.value.isNotBlank()) {
Text(
text = stringResourceSafe(R.string.send_memo, memo.value),
@ -162,64 +182,51 @@ private fun DestinationBlockPreview(
}
private class DestinationBlockPreviewProvider : PreviewParameterProvider<DestinationUM.Content> {
val previewItem = 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,
)
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,
previewItem,
previewItem.copy(
addressTextField = previewItem.addressTextField.copy(
value = "vitalik.eth",
blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
),
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,
previewItem.copy(isRedesignEnabled = true),
previewItem.copy(
addressTextField = previewItem.addressTextField.copy(
value = "vitalik.eth",
blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
),
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,
),
)

View file

@ -142,6 +142,7 @@ private fun LazyListScope.addressItem(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
resolvedAddress = address.blockchainAddress,
)
}
}

View file

@ -39,6 +39,7 @@ internal data object SwapAmountContentPreview {
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "Bitcoin",

View file

@ -107,5 +107,6 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider<ExpressSt
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
}