Updated on 2026-08-14
This commit is contained in:
parent
cd266b50f5
commit
00126b9d36
21 changed files with 711 additions and 16 deletions
|
|
@ -364,6 +364,8 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
private fun verifyAndSendTransaction() {
|
||||
val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
// A blank recipient is deliberately not filtered out here: returning early would leave `isSending` on
|
||||
// forever. It is rejected while the transaction is built, so it surfaces as the regular error alert.
|
||||
val destination = destinationUM?.addressTextField?.actualAddress ?: return
|
||||
val memo = destinationUM?.memoTextField?.value
|
||||
val fee = feeUMV2?.selectedFeeItem?.fee
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ internal class SendDestinationAlertFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun showUnrecognizedQrCodeAlert() {
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(id = R.string.qr_scanner_error_unrecognized_title),
|
||||
message = resourceReference(id = R.string.qr_scanner_error_unrecognized_message),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun showRecipientBackupErrorAlert(onContactSupport: () -> Unit) {
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ 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.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
|
|
@ -314,8 +315,14 @@ internal class SendDestinationModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onQrCodeScanned(address: String) {
|
||||
val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull() ?: return
|
||||
private fun onQrCodeScanned(qrCode: String) {
|
||||
val parsedQrCode = parseQrCodeUseCase(qrCode, cryptoCurrency).getOrNull()
|
||||
|
||||
if (parsedQrCode == null || parsedQrCode.address.isBlank()) {
|
||||
sendDestinationAlertFactory.showUnrecognizedQrCodeAlert()
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.update(
|
||||
SendDestinationPredefinedStateTransformer(
|
||||
address = parsedQrCode.address,
|
||||
|
|
@ -443,6 +450,8 @@ internal class SendDestinationModel @Inject constructor(
|
|||
allowSelfSend = params.isAllowSelfSend,
|
||||
)
|
||||
|
||||
notifyIfQrCodeUnrecognized(type = type, addressValidationResult = addressValidationResult)
|
||||
|
||||
if (addressValidationResult.isRight()) {
|
||||
val problematicWalletId = resolveBackupProblematicWallet(address)
|
||||
if (problematicWalletId != null) {
|
||||
|
|
@ -503,6 +512,21 @@ internal class SendDestinationModel @Inject constructor(
|
|||
}.saveIn(validationJobHolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* The scanned code carries nothing this network accepts as a recipient — a link, an address of another network,
|
||||
* a burn address. Reported as an alert, because the inline field error is easy to miss right after the camera
|
||||
* closes, and a QR that "filled something in" reads as a successfully scanned recipient.
|
||||
*/
|
||||
private fun notifyIfQrCodeUnrecognized(
|
||||
type: EnterAddressSource?,
|
||||
addressValidationResult: AddressValidationResult,
|
||||
) {
|
||||
if (type != EnterAddressSource.QRCode) return
|
||||
if (addressValidationResult.leftOrNull() != AddressValidation.Error.InvalidAddress) return
|
||||
|
||||
sendDestinationAlertFactory.showUnrecognizedQrCodeAlert()
|
||||
}
|
||||
|
||||
private fun recognizeContact(type: EnterAddressSource?, isValidAddress: Boolean, address: String) {
|
||||
if (type == null || type == EnterAddressSource.Contact) return
|
||||
val contact = if (isValidAddress) findContactByAddress(address) else null
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ private fun LazyListScope.addressItem(
|
|||
onAddressChange(it, EnterAddressSource.PasteButton)
|
||||
}
|
||||
},
|
||||
onClearClick = {
|
||||
GlobalMultipleClickPreventer.processEvent {
|
||||
onAddressChange("", EnterAddressSource.InputField)
|
||||
}
|
||||
},
|
||||
onQrCodeClick = onQrCodeClick,
|
||||
isError = isError,
|
||||
isLoading = isValidating,
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ internal fun TextFieldWithPaste(
|
|||
.align(CenterVertically),
|
||||
) {
|
||||
CrossIcon(
|
||||
onClick = onPasteClick,
|
||||
onClick = { onPasteClick("") },
|
||||
modifier = Modifier.testTag(SendAddressScreenTestTags.DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.models.network.CryptoCurrencyAddress
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.qrscanning.models.QrResult
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
|
|
@ -286,7 +287,7 @@ internal class SendDestinationModelTest {
|
|||
inner class QrScan {
|
||||
|
||||
@Test
|
||||
fun `GIVEN unparseable QR WHEN scanned THEN do NOT validate`() = runTest {
|
||||
fun `GIVEN unparseable QR WHEN scanned THEN do NOT validate AND show unrecognized alert`() = runTest {
|
||||
// Arrange
|
||||
val qrFlow = MutableStateFlow("rawQr")
|
||||
every { listenToQrScanningUseCase(any()) } returns qrFlow.right()
|
||||
|
|
@ -307,6 +308,83 @@ internal class SendDestinationModelTest {
|
|||
any()
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) { sendDestinationAlertFactory.showUnrecognizedQrCodeAlert() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN QR without address WHEN scanned THEN keep entered recipient AND show unrecognized alert`() =
|
||||
runTest {
|
||||
// Arrange — the QR parses but yields no address (e.g. an ERC-681 URI for another token)
|
||||
val qrFlow = MutableSharedFlow<String>(extraBufferCapacity = 1)
|
||||
every { listenToQrScanningUseCase(any()) } returns qrFlow.right()
|
||||
every { parseQrCodeUseCase("rawQr", cryptoCurrency) } returns QrResult(address = "").right()
|
||||
val sut = buildModel(initialState = contentState(address = "0xEntered"))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
qrFlow.tryEmit("rawQr")
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — a QR with nothing usable must not wipe what the user already has
|
||||
assertThat(content(sut).addressTextField.value).isEqualTo("0xEntered")
|
||||
verify(exactly = 1) { sendDestinationAlertFactory.showUnrecognizedQrCodeAlert() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN QR address invalid for network WHEN scanned THEN show unrecognized alert`() = runTest {
|
||||
// Arrange — a scanned link or a foreign-network address the current network rejects
|
||||
val qrFlow = MutableStateFlow("rawQr")
|
||||
every { listenToQrScanningUseCase(any()) } returns qrFlow.right()
|
||||
every { parseQrCodeUseCase("rawQr", cryptoCurrency) } returns QrResult(address = "exchange.com").right()
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(any(), any(), eq("exchange.com"), any<List<CryptoCurrencyAddress>>(), any())
|
||||
} returns AddressValidation.Error.InvalidAddress.left()
|
||||
|
||||
// Act
|
||||
buildModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { sendDestinationAlertFactory.showUnrecognizedQrCodeAlert() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN QR address already in wallet WHEN scanned THEN do NOT show unrecognized alert`() = runTest {
|
||||
// Arrange — self-send is reported inline on the field, the QR itself was read fine
|
||||
val qrFlow = MutableStateFlow("rawQr")
|
||||
every { listenToQrScanningUseCase(any()) } returns qrFlow.right()
|
||||
every { parseQrCodeUseCase("rawQr", cryptoCurrency) } returns QrResult(address = "0xSelf").right()
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(any(), any(), eq("0xSelf"), any<List<CryptoCurrencyAddress>>(), any())
|
||||
} returns AddressValidation.Error.AddressInWallet.left()
|
||||
|
||||
// Act
|
||||
buildModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { sendDestinationAlertFactory.showUnrecognizedQrCodeAlert() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN QR with valid address WHEN scanned THEN fill recipient without alert`() = runTest {
|
||||
// Arrange
|
||||
val qrFlow = MutableStateFlow("rawQr")
|
||||
every { listenToQrScanningUseCase(any()) } returns qrFlow.right()
|
||||
every { parseQrCodeUseCase("rawQr", cryptoCurrency) } returns
|
||||
QrResult(address = "0xValid", memo = "42").right()
|
||||
coEvery {
|
||||
validateWalletAddressUseCase(any(), any(), eq("0xValid"), any<List<CryptoCurrencyAddress>>(), any())
|
||||
} returns AddressValidation.Success.Valid.right()
|
||||
val sut = buildModel(initialState = contentState(address = "", memo = ""))
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(content(sut).addressTextField.value).isEqualTo("0xValid")
|
||||
assertThat(content(sut).memoTextField?.value).isEqualTo("42")
|
||||
verify(exactly = 0) { sendDestinationAlertFactory.showUnrecognizedQrCodeAlert() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue