Updated on 2026-08-14
This commit is contained in:
parent
9477b2f497
commit
774898f560
3 changed files with 230 additions and 11 deletions
|
|
@ -23,6 +23,8 @@ import com.tangem.domain.qrscanning.models.SourceType
|
|||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
|
|
@ -61,6 +63,7 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val isMemoRequiredUseCase: IsMemoRequiredUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||
|
|
@ -295,6 +298,20 @@ internal class SendDestinationModel @Inject constructor(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
memo = memo.orEmpty(),
|
||||
)
|
||||
val resolvedAddress =
|
||||
(addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)
|
||||
?.blockchainAddress
|
||||
?: address
|
||||
// Ripple X-Address already embeds the destination tag, so memo is irrelevant for it
|
||||
val isXAddress = addressValidationResult.getOrNull() == AddressValidation.Success.ValidXAddress
|
||||
val isMemoRequired = memo.isNullOrBlank() &&
|
||||
!isXAddress &&
|
||||
addressValidationResult.isRight() &&
|
||||
(uiState.value as? DestinationUM.Content)?.memoTextField != null &&
|
||||
isMemoRequiredUseCase(
|
||||
network = cryptoCurrency.network,
|
||||
destinationAddress = resolvedAddress,
|
||||
)
|
||||
|
||||
if (type != null) {
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -308,12 +325,17 @@ internal class SendDestinationModel @Inject constructor(
|
|||
}
|
||||
_uiState.update(
|
||||
SendDestinationValidationResultTransformer(
|
||||
addressValidationResult,
|
||||
memoValidationResult,
|
||||
addressValidationResult = addressValidationResult,
|
||||
memoValidationResult = memoValidationResult,
|
||||
isMemoRequired = isMemoRequired,
|
||||
),
|
||||
)
|
||||
if (type != null) {
|
||||
autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight())
|
||||
autoNextFromRecipient(
|
||||
type = type,
|
||||
isValidAddress = addressValidationResult.isRight(),
|
||||
isValidMemo = isXAddress || memoValidationResult.isRight() && !isMemoRequired,
|
||||
)
|
||||
}
|
||||
}.saveIn(validationJobHolder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.error.ValidateMemoError
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
|
@ -14,12 +15,14 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
internal class SendDestinationValidationResultTransformer(
|
||||
private val addressValidationResult: AddressValidationResult,
|
||||
private val memoValidationResult: Either<ValidateMemoError, Unit>,
|
||||
private val isMemoRequired: Boolean = false,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
val shouldDisableMemo = shouldDisableMemo()
|
||||
val isValidAddress = addressValidationResult.isRight()
|
||||
val isValidMemo = memoValidationResult.isRight()
|
||||
val isValidMemo = shouldDisableMemo || memoValidationResult.isRight()
|
||||
|
||||
val addressErrorText = addressValidationResult.mapLeft { error ->
|
||||
when (error) {
|
||||
|
|
@ -30,23 +33,23 @@ internal class SendDestinationValidationResultTransformer(
|
|||
}
|
||||
}.leftOrNull()
|
||||
|
||||
val shouldDisableMemo = shouldDisableMemo()
|
||||
val blockchainAddress =
|
||||
(addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress
|
||||
|
||||
val memoField = state.memoTextField
|
||||
val isMemoMissing = isMemoRequired && !shouldDisableMemo && state.memoTextField?.value.isNullOrBlank()
|
||||
return state.copy(
|
||||
isValidating = false,
|
||||
isPrimaryButtonEnabled = isValidAddress && isValidMemo,
|
||||
isPrimaryButtonEnabled = isValidAddress && isValidMemo && !isMemoMissing,
|
||||
addressTextField = state.addressTextField.copy(
|
||||
error = addressErrorText?.let(::resourceReference),
|
||||
isError = state.addressTextField.value.isNotEmpty() && !isValidAddress,
|
||||
blockchainAddress = blockchainAddress,
|
||||
),
|
||||
memoTextField = memoField?.copy(
|
||||
value = memoField.value.takeIf { !shouldDisableMemo }.orEmpty(),
|
||||
isError = memoField.value.isNotEmpty() && !isValidMemo,
|
||||
isEnabled = !shouldDisableMemo,
|
||||
memoTextField = buildMemoField(
|
||||
memoField = state.memoTextField,
|
||||
isValidMemo = isValidMemo,
|
||||
isMemoMissing = isMemoMissing,
|
||||
shouldDisableMemo = shouldDisableMemo,
|
||||
),
|
||||
recent = state.recent.map { recent ->
|
||||
recent.copy(isVisible = !isValidAddress && (recent.isLoading || recent.title != TextReference.EMPTY))
|
||||
|
|
@ -58,6 +61,26 @@ internal class SendDestinationValidationResultTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildMemoField(
|
||||
memoField: DestinationTextFieldUM.RecipientMemo?,
|
||||
isValidMemo: Boolean,
|
||||
isMemoMissing: Boolean,
|
||||
shouldDisableMemo: Boolean,
|
||||
): DestinationTextFieldUM.RecipientMemo? {
|
||||
memoField ?: return null
|
||||
val isMemoFormatError = memoField.value.isNotEmpty() && !isValidMemo
|
||||
return memoField.copy(
|
||||
value = memoField.value.takeIf { !shouldDisableMemo }.orEmpty(),
|
||||
isError = isMemoFormatError || isMemoMissing,
|
||||
error = if (isMemoMissing) {
|
||||
resourceReference(R.string.send_validation_destination_tag_required_title)
|
||||
} else {
|
||||
resourceReference(R.string.send_memo_destination_tag_error)
|
||||
},
|
||||
isEnabled = !shouldDisableMemo,
|
||||
)
|
||||
}
|
||||
|
||||
/** Ripple X-Address contains memo, so memo field is unnecessary */
|
||||
private fun shouldDisableMemo(): Boolean {
|
||||
return addressValidationResult.isRight { it == AddressValidation.Success.ValidXAddress }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.error.ValidateMemoError
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SendDestinationValidationResultTransformerTest {
|
||||
|
||||
private val validAddress: AddressValidationResult = AddressValidation.Success.Valid.right()
|
||||
private val invalidAddress: AddressValidationResult = AddressValidation.Error.InvalidAddress.left()
|
||||
private val xAddress: AddressValidationResult = AddressValidation.Success.ValidXAddress.right()
|
||||
|
||||
private val validMemo = Unit.right()
|
||||
private val invalidMemo = ValidateMemoError.InvalidMemo.left()
|
||||
|
||||
private val formatErrorRef = resourceReference(R.string.send_memo_destination_tag_error)
|
||||
private val tagRequiredRef = resourceReference(R.string.send_validation_destination_tag_required_title)
|
||||
|
||||
@Test
|
||||
fun `GIVEN required memo is empty WHEN transform THEN tag required error shown and primary button disabled`() {
|
||||
val result = transform(validAddress, validMemo, isMemoRequired = true, memo = "")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
assertThat(result.memoTextField?.isError).isTrue()
|
||||
assertThat(result.memoTextField?.error).isEqualTo(tagRequiredRef)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN required memo is filled with valid value WHEN transform THEN primary button enabled`() {
|
||||
val result = transform(validAddress, validMemo, isMemoRequired = true, memo = "123")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
assertThat(result.memoTextField?.isError).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN required memo filled with whitespace only WHEN transform THEN tag required error shown and primary button disabled`() {
|
||||
val result = transform(validAddress, validMemo, isMemoRequired = true, memo = " ")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
assertThat(result.memoTextField?.isError).isTrue()
|
||||
assertThat(result.memoTextField?.error).isEqualTo(tagRequiredRef)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty memo that is not required WHEN transform THEN field valid and primary button enabled`() {
|
||||
val result = transform(validAddress, validMemo, isMemoRequired = false, memo = "")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
assertThat(result.memoTextField?.isError).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entered memo with valid format WHEN transform THEN primary button enabled`() {
|
||||
val result = transform(validAddress, validMemo, isMemoRequired = false, memo = "valid-memo")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
assertThat(result.memoTextField?.isError).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entered memo with invalid format WHEN transform THEN format error shown and primary button disabled`() {
|
||||
val result = transform(validAddress, invalidMemo, isMemoRequired = false, memo = "bad-memo")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
assertThat(result.memoTextField?.isError).isTrue()
|
||||
assertThat(result.memoTextField?.error).isEqualTo(formatErrorRef)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN invalid memo after prior tag required state WHEN transform THEN format error shown`() {
|
||||
val staleState = contentState(memo = "bad-memo").let { state ->
|
||||
state.copy(memoTextField = state.memoTextField?.copy(error = tagRequiredRef))
|
||||
}
|
||||
|
||||
val result = SendDestinationValidationResultTransformer(
|
||||
addressValidationResult = validAddress,
|
||||
memoValidationResult = invalidMemo,
|
||||
isMemoRequired = false,
|
||||
).transform(staleState) as DestinationUM.Content
|
||||
|
||||
assertThat(result.memoTextField?.isError).isTrue()
|
||||
assertThat(result.memoTextField?.error).isEqualTo(formatErrorRef)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN invalid address WHEN transform THEN primary button disabled`() {
|
||||
val result = transform(invalidAddress, validMemo, isMemoRequired = false, memo = "123")
|
||||
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN x-address with required memo WHEN transform THEN memo field disabled cleared and primary button enabled`() {
|
||||
val result = transform(xAddress, validMemo, isMemoRequired = true, memo = "")
|
||||
|
||||
assertThat(result.memoTextField?.isEnabled).isFalse()
|
||||
assertThat(result.memoTextField?.isError).isFalse()
|
||||
assertThat(result.memoTextField?.value).isEmpty()
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN x-address with stale invalid memo WHEN transform THEN memo ignored field disabled and primary button enabled`() {
|
||||
val result = transform(xAddress, invalidMemo, isMemoRequired = false, memo = "stale-memo")
|
||||
|
||||
assertThat(result.memoTextField?.isEnabled).isFalse()
|
||||
assertThat(result.memoTextField?.isError).isFalse()
|
||||
assertThat(result.memoTextField?.value).isEmpty()
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN network without memo field WHEN transform THEN memo field is null and primary button enabled`() {
|
||||
val state = contentState(memo = "").copy(memoTextField = null)
|
||||
|
||||
val result = SendDestinationValidationResultTransformer(
|
||||
addressValidationResult = validAddress,
|
||||
memoValidationResult = validMemo,
|
||||
isMemoRequired = false,
|
||||
).transform(state) as DestinationUM.Content
|
||||
|
||||
assertThat(result.memoTextField).isNull()
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
}
|
||||
|
||||
private fun transform(
|
||||
address: AddressValidationResult,
|
||||
memoResult: Either<ValidateMemoError, Unit>,
|
||||
isMemoRequired: Boolean,
|
||||
memo: String,
|
||||
): DestinationUM.Content = SendDestinationValidationResultTransformer(
|
||||
addressValidationResult = address,
|
||||
memoValidationResult = memoResult,
|
||||
isMemoRequired = isMemoRequired,
|
||||
).transform(contentState(memo = memo)) as DestinationUM.Content
|
||||
|
||||
private fun contentState(memo: String) = DestinationUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
addressTextField = DestinationTextFieldUM.RecipientAddress(
|
||||
value = "0xRecipient",
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
placeholder = TextReference.EMPTY,
|
||||
label = TextReference.EMPTY,
|
||||
isValuePasted = false,
|
||||
),
|
||||
memoTextField = DestinationTextFieldUM.RecipientMemo(
|
||||
value = memo,
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
placeholder = TextReference.EMPTY,
|
||||
label = TextReference.EMPTY,
|
||||
error = formatErrorRef,
|
||||
disabledText = TextReference.EMPTY,
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
),
|
||||
recent = persistentListOf(),
|
||||
wallets = persistentListOf(),
|
||||
networkName = "Ethereum",
|
||||
isRecentHidden = false,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue