Updated on 2026-08-14
This commit is contained in:
parent
0b43caf853
commit
2e581a252f
6 changed files with 81 additions and 19 deletions
|
|
@ -25,6 +25,10 @@ dependencies {
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region Libs
|
||||||
|
implementation(projects.libs.crypto)
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region Domain
|
// region Domain
|
||||||
api(projects.domain.common)
|
api(projects.domain.common)
|
||||||
api(projects.domain.tokens)
|
api(projects.domain.tokens)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.usecase
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.lib.crypto.BlockchainUtils
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
|
@ -30,7 +31,10 @@ class GetContactsUseCase(
|
||||||
private fun Contact.matches(query: String): Boolean {
|
private fun Contact.matches(query: String): Boolean {
|
||||||
val isNameContaining = name.value.contains(other = query, ignoreCase = true)
|
val isNameContaining = name.value.contains(other = query, ignoreCase = true)
|
||||||
val isAddressContaining = addresses.any { addressEntry ->
|
val isAddressContaining = addresses.any { addressEntry ->
|
||||||
addressEntry.address.contains(other = query, ignoreCase = false)
|
val isCaseInsensitiveContractAddress = BlockchainUtils.isCaseInsensitiveContractAddress(
|
||||||
|
networkId = addressEntry.networkId.value,
|
||||||
|
)
|
||||||
|
addressEntry.address.contains(other = query, ignoreCase = isCaseInsensitiveContractAddress)
|
||||||
}
|
}
|
||||||
val isNetworkContaining = addresses.any { addressEntry ->
|
val isNetworkContaining = addresses.any { addressEntry ->
|
||||||
addressEntry.networkId.value.contains(other = query, ignoreCase = true)
|
addressEntry.networkId.value.contains(other = query, ignoreCase = true)
|
||||||
|
|
|
||||||
|
|
@ -54,31 +54,44 @@ class GetContactsUseCaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN address query matching case WHEN invoke THEN returns matching contact`() = runTest {
|
fun `GIVEN EVM address query differing only in case WHEN invoke THEN returns matching contact`() = runTest {
|
||||||
// Arrange
|
// Arrange — EVM (ethereum) addresses are case-insensitive, so a lowercased query matches a checksummed address
|
||||||
val carol = contact(name = "Carol", address = "0xAbCdEf")
|
val carol = contact(name = "Carol", address = "0xAbCdEf", networkId = "ethereum")
|
||||||
every { repository.getAllContacts() } returns flowOf(listOf(alice, carol))
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = useCase(query = "0xAbCdEf").first()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result).containsExactly(carol)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN address query with different case WHEN invoke THEN returns empty`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
val carol = contact(name = "Carol", address = "0xAbCdEf")
|
|
||||||
every { repository.getAllContacts() } returns flowOf(listOf(alice, carol))
|
every { repository.getAllContacts() } returns flowOf(listOf(alice, carol))
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
val result = useCase(query = "0xabcdef").first()
|
val result = useCase(query = "0xabcdef").first()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).containsExactly(carol)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-EVM address query differing only in case WHEN invoke THEN returns empty`() = runTest {
|
||||||
|
// Arrange — non-EVM (solana) addresses are case-sensitive, so a differently-cased query must not match
|
||||||
|
val dave = contact(name = "Dave", address = "SoLAnaAddr", networkId = "solana")
|
||||||
|
every { repository.getAllContacts() } returns flowOf(listOf(alice, dave))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(query = "solanaaddr").first()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).isEmpty()
|
assertThat(result).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-EVM address query with exact case WHEN invoke THEN returns matching contact`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val dave = contact(name = "Dave", address = "SoLAnaAddr", networkId = "solana")
|
||||||
|
every { repository.getAllContacts() } returns flowOf(listOf(alice, dave))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(query = "SoLAnaAddr").first()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).containsExactly(dave)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN query matches a network id WHEN invoke THEN returns only contacts on that network`() = runTest {
|
fun `GIVEN query matches a network id WHEN invoke THEN returns only contacts on that network`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ internal class AddressBookListModel @Inject constructor(
|
||||||
.shareIn(modelScope, SharingStarted.Lazily, replay = 1)
|
.shareIn(modelScope, SharingStarted.Lazily, replay = 1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
modelScope.launch(context = dispatchers.default) {
|
modelScope.launch {
|
||||||
syncAddressBooksUseCase()
|
syncAddressBooksUseCase()
|
||||||
sendContactListScreenOpenedEvent()
|
sendContactListScreenOpenedEvent()
|
||||||
observeContacts()
|
observeContacts()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
|
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||||
import com.tangem.features.addressbook.AddressBookSendAnalytics
|
import com.tangem.features.addressbook.AddressBookSendAnalytics
|
||||||
import com.tangem.features.addressbook.ContactSelectionListener
|
import com.tangem.features.addressbook.ContactSelectionListener
|
||||||
import com.tangem.features.addressbook.MatchedContact
|
import com.tangem.features.addressbook.MatchedContact
|
||||||
|
|
@ -90,6 +91,7 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
private val addressBookSendAnalytics: AddressBookSendAnalytics,
|
private val addressBookSendAnalytics: AddressBookSendAnalytics,
|
||||||
private val syncAddressBooksUseCase: SyncAddressBooksUseCase,
|
private val syncAddressBooksUseCase: SyncAddressBooksUseCase,
|
||||||
|
private val addressBookFeatureToggles: AddressBookFeatureToggles,
|
||||||
getContactsUseCase: GetContactsUseCase,
|
getContactsUseCase: GetContactsUseCase,
|
||||||
contactSelectionListener: ContactSelectionListener,
|
contactSelectionListener: ContactSelectionListener,
|
||||||
) : Model(), SendDestinationClickIntents {
|
) : Model(), SendDestinationClickIntents {
|
||||||
|
|
@ -141,7 +143,7 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() }
|
syncAddressBooksIfNeeded()
|
||||||
subscribeOnQRScannerResult()
|
subscribeOnQRScannerResult()
|
||||||
initialState()
|
initialState()
|
||||||
resetContactOnEdit()
|
resetContactOnEdit()
|
||||||
|
|
@ -289,6 +291,12 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
}.launchIn(modelScope)
|
}.launchIn(modelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun syncAddressBooksIfNeeded() {
|
||||||
|
if (addressBookFeatureToggles.isAddressBookEnabled) {
|
||||||
|
modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun subscribeOnQRScannerResult() {
|
private fun subscribeOnQRScannerResult() {
|
||||||
listenToQrScanningUseCase(SourceType.SEND)
|
listenToQrScanningUseCase(SourceType.SEND)
|
||||||
.getOrElse { emptyFlow() }
|
.getOrElse { emptyFlow() }
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
|
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||||
import com.tangem.features.addressbook.AddressBookSendAnalytics
|
import com.tangem.features.addressbook.AddressBookSendAnalytics
|
||||||
import com.tangem.features.addressbook.ContactSelectionListener
|
import com.tangem.features.addressbook.ContactSelectionListener
|
||||||
import com.tangem.features.addressbook.MatchedContact
|
import com.tangem.features.addressbook.MatchedContact
|
||||||
|
|
@ -95,6 +96,7 @@ internal class SendDestinationModelTest {
|
||||||
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true)
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true)
|
||||||
private val getContactsUseCase: GetContactsUseCase = mockk(relaxed = true)
|
private val getContactsUseCase: GetContactsUseCase = mockk(relaxed = true)
|
||||||
private val syncAddressBooksUseCase: SyncAddressBooksUseCase = mockk(relaxed = true)
|
private val syncAddressBooksUseCase: SyncAddressBooksUseCase = mockk(relaxed = true)
|
||||||
|
private val addressBookFeatureToggles: AddressBookFeatureToggles = mockk(relaxed = true)
|
||||||
private val contactSelectionListener: ContactSelectionListener = mockk(relaxed = true)
|
private val contactSelectionListener: ContactSelectionListener = mockk(relaxed = true)
|
||||||
private val addressBookSendAnalytics: AddressBookSendAnalytics = mockk(relaxed = true)
|
private val addressBookSendAnalytics: AddressBookSendAnalytics = mockk(relaxed = true)
|
||||||
private val callback: SendDestinationComponent.ModelCallback = mockk(relaxed = true)
|
private val callback: SendDestinationComponent.ModelCallback = mockk(relaxed = true)
|
||||||
|
|
@ -515,6 +517,36 @@ internal class SendDestinationModelTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class SyncAddressBooks {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN address book enabled WHEN model initialized THEN sync address books`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { addressBookFeatureToggles.isAddressBookEnabled } returns true
|
||||||
|
|
||||||
|
// Act
|
||||||
|
buildModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { syncAddressBooksUseCase() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN address book disabled WHEN model initialized THEN do NOT sync address books`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
every { addressBookFeatureToggles.isAddressBookEnabled } returns false
|
||||||
|
|
||||||
|
// Act
|
||||||
|
buildModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { syncAddressBooksUseCase() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// region fixtures
|
// region fixtures
|
||||||
|
|
||||||
private fun TestScope.buildModel(
|
private fun TestScope.buildModel(
|
||||||
|
|
@ -574,6 +606,7 @@ internal class SendDestinationModelTest {
|
||||||
sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase,
|
sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase,
|
||||||
addressBookSendAnalytics = addressBookSendAnalytics,
|
addressBookSendAnalytics = addressBookSendAnalytics,
|
||||||
syncAddressBooksUseCase = syncAddressBooksUseCase,
|
syncAddressBooksUseCase = syncAddressBooksUseCase,
|
||||||
|
addressBookFeatureToggles = addressBookFeatureToggles,
|
||||||
getContactsUseCase = getContactsUseCase,
|
getContactsUseCase = getContactsUseCase,
|
||||||
contactSelectionListener = contactSelectionListener,
|
contactSelectionListener = contactSelectionListener,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue