Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:51:02 +01:00
parent e638377a5e
commit b741ed5d02
18 changed files with 578 additions and 366 deletions

View file

@ -20,7 +20,7 @@ import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.model.ContactName
import com.tangem.domain.addressbook.usecase.DeleteContactUseCase
import com.tangem.domain.addressbook.usecase.GetContactByIdUseCase
import com.tangem.domain.addressbook.usecase.ValidateContactNameUseCase
import com.tangem.domain.addressbook.validation.ContactNameValidator
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.wallet.UserWallet
@ -55,7 +55,7 @@ internal class EditContactModel @Inject constructor(
private val resultHolder: AddressBookResultHolder,
private val messageSender: UiMessageSender,
private val userWalletsListRepository: UserWalletsListRepository,
private val validateContactNameUseCase: ValidateContactNameUseCase,
private val contactNameValidator: ContactNameValidator,
private val saveContactInteractor: SaveContactInteractor,
private val getContactByIdUseCase: GetContactByIdUseCase,
private val deleteContactUseCase: DeleteContactUseCase,
@ -417,7 +417,7 @@ internal class EditContactModel @Inject constructor(
private suspend fun validateName(name: String, walletId: UserWalletId): TextReference? {
if (name.isBlank()) return null
if (name == loadedContact.value?.name?.value) return null
val error = validateContactNameUseCase(walletId, name).leftOrNull() ?: return null
val error = contactNameValidator.validate(walletId, name).leftOrNull() ?: return null
if (error is ContactNameValidationError.Format && error.error is ContactName.Error.Empty) return null
return ContactNameErrorConverter().convert(error)
}

View file

@ -58,12 +58,16 @@ internal class AddressBookListModel @Inject constructor(
private val selectedWalletId = MutableStateFlow<String?>(value = null)
private val allContacts: SharedFlow<List<VerifiedContact>> =
getVerifiedContactsInteractor(query = "", userWalletId = null)
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
.shareIn(modelScope, SharingStarted.Lazily, replay = 1)
init {
val matchedContacts = searchQuery.flatMapLatest { query ->
if (query.isBlank()) allContacts else getVerifiedContactsInteractor(query = query, userWalletId = null)
if (query.isBlank()) {
allContacts
} else {
getVerifiedContactsInteractor.getVerifiedContacts(query = query, userWalletId = null)
}
}
combine(
allContacts,

View file

@ -19,7 +19,7 @@ import com.tangem.domain.addressbook.interactor.SaveContactInteractor
import com.tangem.domain.addressbook.model.*
import com.tangem.domain.addressbook.usecase.DeleteContactUseCase
import com.tangem.domain.addressbook.usecase.GetContactByIdUseCase
import com.tangem.domain.addressbook.usecase.ValidateContactNameUseCase
import com.tangem.domain.addressbook.validation.ContactNameValidator
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.CryptoPortfolioIcon
@ -56,7 +56,7 @@ internal class EditContactModelTest {
private val resultHolder = AddressBookResultHolder()
private val messageSender: UiMessageSender = mockk(relaxed = true)
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxed = true)
private val validateContactNameUseCase: ValidateContactNameUseCase = mockk()
private val contactNameValidator: ContactNameValidator = mockk()
private val saveContactInteractor: SaveContactInteractor = mockk()
private val getContactByIdUseCase: GetContactByIdUseCase = mockk()
private val deleteContactUseCase: DeleteContactUseCase = mockk()
@ -75,7 +75,7 @@ internal class EditContactModelTest {
fun setUp() {
// Default: no wallets loaded, name always valid. Individual tests override as needed.
setupWallets(wallets = emptyList(), selected = null)
coEvery { validateContactNameUseCase(any(), any()) } returns ContactName("Satoshi").getOrNull()!!.right()
coEvery { contactNameValidator.validate(any(), any()) } returns ContactName("Satoshi").getOrNull()!!.right()
every { portfolioFetcherFactory.create(any(), any()) } returns portfolioFetcher
every { portfolioSelectorController.selectedAccountWithData(any()) } returns selectedWalletData
// No existing contact by default; a StateFlow<null> never surfaces a contact and never completes.
@ -396,7 +396,7 @@ internal class EditContactModelTest {
val walletA = createWallet(id = "aa", name = "Wallet A")
setupWallets(wallets = listOf(walletA), selected = walletA)
coEvery {
validateContactNameUseCase(any(), any())
contactNameValidator.validate(any(), any())
} returns ContactNameValidationError.Duplicate.left()
val model = createModel(testScope = this)
advanceUntilIdle()
@ -434,7 +434,7 @@ internal class EditContactModelTest {
val walletB = createWallet(id = "bb", name = "Wallet B")
setupWallets(wallets = listOf(walletA, walletB), selected = walletA)
coEvery {
validateContactNameUseCase(walletB.walletId, "Satoshi")
contactNameValidator.validate(walletB.walletId, "Satoshi")
} returns ContactNameValidationError.Duplicate.left()
val model = createModel(testScope = this)
advanceUntilIdle()
@ -489,7 +489,7 @@ internal class EditContactModelTest {
// Arrange
val walletA = createWallet(id = "aa", name = "Wallet A")
setupWallets(wallets = listOf(walletA), selected = walletA)
coEvery { validateContactNameUseCase(any(), any()) } returns ContactNameValidationError.Duplicate.left()
coEvery { contactNameValidator.validate(any(), any()) } returns ContactNameValidationError.Duplicate.left()
val model = createModel(testScope = this)
advanceUntilIdle()
@ -927,7 +927,7 @@ internal class EditContactModelTest {
resultHolder = resultHolder,
messageSender = messageSender,
userWalletsListRepository = userWalletsListRepository,
validateContactNameUseCase = validateContactNameUseCase,
contactNameValidator = contactNameValidator,
saveContactInteractor = saveContactInteractor,
getContactByIdUseCase = getContactByIdUseCase,
deleteContactUseCase = deleteContactUseCase,

View file

@ -61,7 +61,7 @@ internal class AddressBookListModelTest {
@Test
fun `GIVEN feature just opened WHEN contacts not yet loaded THEN Loading state`() = runTest {
// Arrange — the interactor has not emitted yet (books still syncing).
every { getVerifiedContactsInteractor(query = "", userWalletId = null) } returns emptyFlow()
every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns emptyFlow()
// Act
val model = createModel(testScope = this, mode = AddressBookRoute.ListMode.Default)
@ -73,7 +73,7 @@ internal class AddressBookListModelTest {
@Test
fun `GIVEN default mode AND verified contacts WHEN created THEN content shown`() = runTest {
// Arrange
every { getVerifiedContactsInteractor(query = "", userWalletId = null) } returns
every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns
flowOf(listOf(verifiedContact(id = "1", name = "Alice"), verifiedContact(id = "2", name = "Bob")))
// Act
@ -89,7 +89,7 @@ internal class AddressBookListModelTest {
@Test
fun `GIVEN default mode AND no contacts WHEN created THEN empty state`() = runTest {
// Arrange
every { getVerifiedContactsInteractor(query = "", userWalletId = null) } returns flowOf(emptyList())
every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns flowOf(emptyList())
// Act
val model = createModel(testScope = this, mode = AddressBookRoute.ListMode.Default)
@ -103,7 +103,7 @@ internal class AddressBookListModelTest {
fun `GIVEN default mode WHEN contact clicked THEN editor opened with contact id`() = runTest {
// Arrange
var clickedId: String? = null
every { getVerifiedContactsInteractor(query = "", userWalletId = null) } returns
every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns
flowOf(listOf(verifiedContact(id = "42", name = "Alice")))
val model = createModel(
testScope = this,