Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-05 21:00:06 +03:00
parent 946d60c1c6
commit 9c11182ca6
14 changed files with 127 additions and 40 deletions

View file

@ -449,6 +449,7 @@
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>

View file

@ -448,6 +448,7 @@
<string name="scan_card_settings_button">Scan Card</string>
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
@ -89,11 +90,16 @@ fun SimpleTextField(
decorationBox = decorationBox ?: { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
AnimatedContent(
targetState = placeholder,
label = "Placeholder Change Animation",
) {
Text(
text = it.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
}
textValue()
}

View file

@ -46,6 +46,9 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.navigation)
/** Libs */
implementation(projects.libs.crypto)
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.legacy)

View file

@ -136,12 +136,13 @@ internal class SendStateFactory(
)
}
fun onRecipientAddressValueChange(value: String): SendUiState {
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
),
)
}
@ -216,6 +217,20 @@ internal class SendStateFactory(
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
isEnabled = true,
),
),
)
}
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
isEnabled = false,
),
),
)

View file

@ -47,6 +47,8 @@ internal sealed class SendTextField {
val label: TextReference,
val isError: Boolean = false,
val error: TextReference? = null,
val disabledText: TextReference,
val isEnabled: Boolean,
) : SendTextField()
data class CustomFee(

View file

@ -44,6 +44,8 @@ internal class SendRecipientMemoFieldConverter(
placeholder = resourceReference(R.string.send_optional_field),
label = resourceReference(value),
error = resourceReference(R.string.send_memo_destination_tag_error),
disabledText = resourceReference(R.string.send_additional_field_already_included),
isEnabled = true,
)
}
}

View file

@ -73,16 +73,18 @@ internal fun SendRecipientContent(
}
uiState.memoTextField?.let { memoField ->
item(key = MEMO_FIELD_KEY) {
val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText
TextFieldWithPaste(
value = memoField.value,
label = memoField.label,
placeholder = memoField.placeholder,
placeholder = placeholder,
footer = stringResource(R.string.send_recipient_memo_footer),
onValueChange = memoField.onValueChange,
onPasteClick = clickIntents::onRecipientMemoValueChange,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
isError = memoField.isError,
error = memoField.error,
isReadOnly = !memoField.isEnabled,
)
}
}

View file

@ -27,11 +27,12 @@ internal fun TextFieldWithPaste(
footer: String? = null,
error: TextReference? = null,
isError: Boolean = false,
isReadOnly: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
val (title, color) = when {
isError && error != null -> error to TangemTheme.colors.text.warning
isReadOnly -> label to TangemTheme.colors.text.disabled
else -> label to TangemTheme.colors.text.secondary
}
FooterContainer(modifier, footer) {
Row(
@ -55,18 +56,21 @@ internal fun TextFieldWithPaste(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
readOnly = isReadOnly,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing6),
)
}
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(end = TangemTheme.dimens.spacing16),
)
if (!isReadOnly) {
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(end = TangemTheme.dimens.spacing16),
)
}
}
}
}

View file

@ -7,8 +7,6 @@ import androidx.lifecycle.*
import androidx.paging.PagingData
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.extensions.isZero
@ -20,8 +18,8 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -43,6 +41,7 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFacto
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -301,10 +300,12 @@ internal class SendViewModel @Inject constructor(
userWalletId = wallet.walletId,
network = walletCurrency.network,
)
return@fold AvailableWallet(
name = wallet.name,
address = addresses.first().value,
)
return@fold addresses.firstOrNull()?.let {
AvailableWallet(
name = wallet.name,
address = it.value,
)
}
},
ifLeft = { null },
)
@ -426,10 +427,10 @@ internal class SendViewModel @Inject constructor(
}
override fun onRecipientAddressValueChange(value: String) {
uiState = stateFactory.onRecipientAddressValueChange(value)
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getOnRecipientAddressValidationStarted()
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.onRecipientAddressValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
}
@ -437,10 +438,10 @@ internal class SendViewModel @Inject constructor(
}
override fun onRecipientMemoValueChange(value: String) {
uiState = stateFactory.getOnRecipientMemoValueChange(value)
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getOnRecipientAddressValidationStarted()
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientMemoValueChange(value)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
}
@ -455,16 +456,14 @@ internal class SendViewModel @Inject constructor(
).getOrElse { false }
}
private fun checkIfXrpAddressValue(value: String): Boolean {
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) {
viewModelScope.launch(dispatchers.io) {
val result = XrpAddressService.decodeXAddress(value)
onRecipientAddressValueChange(result?.address.orEmpty())
onRecipientMemoValueChange(result?.destinationTag.toString())
}
return true
}
return false
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true)
uiState = stateFactory.getOnXAddressMemoState()
val isValidAddress = validateAddress(decodedAddress.address)
uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
true
} ?: false
}
// endregion
@ -665,7 +664,6 @@ internal class SendViewModel @Inject constructor(
// endregion
companion object {
private const val XRP_X_ADDRESS = 'X'
private const val CHECK_FEE_UPDATE_DELAY = 60_000L
private const val BALANCE_UPDATE_DELAY = 10_000L
}

View file

@ -13,4 +13,7 @@ dependencies {
/** SDK */
implementation(deps.tangem.blockchain)
/** Core */
implementation(projects.core.utils)
}

View file

@ -0,0 +1,29 @@
package com.tangem.lib.crypto
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter
import com.tangem.lib.crypto.models.XrpTaggedAddress
/**
* !!!IMPORTANT!!!
* Methods for working with different blockchains
* All methods are depend on specific blockchain or check for specific blockchain
*
* Temporary solution for domain specific logic for Blockchain.
* Instead of creating repositories and unnecessary and overkill use cases
*/
object BlockchainUtils {
private const val XRP_X_ADDRESS = 'X'
/** Decodes XRP Blockchain address */
fun decodeRippleXAddress(xAddress: String, networkId: String): XrpTaggedAddress? {
return if (networkId == Blockchain.XRP.id && xAddress.firstOrNull() == XRP_X_ADDRESS) {
val decodedAddress = XrpAddressService.decodeXAddress(xAddress)
return decodedAddress?.let(XrpTaggedAddressConverter()::convert)
} else {
null
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.lib.crypto.converter
import com.tangem.lib.crypto.models.XrpTaggedAddress
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.blockchains.xrp.XrpTaggedAddress as BlockchainXrpTaggedAddress
internal class XrpTaggedAddressConverter : Converter<BlockchainXrpTaggedAddress, XrpTaggedAddress> {
override fun convert(value: BlockchainXrpTaggedAddress): XrpTaggedAddress {
return XrpTaggedAddress(
address = value.address,
destinationTag = value.destinationTag,
)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.lib.crypto.models
data class XrpTaggedAddress(
val address: String,
val destinationTag: Long?,
)