diff --git a/data/address-book/src/main/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepository.kt b/data/address-book/src/main/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepository.kt index 2fbb2ee921..7638d8d970 100644 --- a/data/address-book/src/main/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepository.kt +++ b/data/address-book/src/main/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepository.kt @@ -50,7 +50,6 @@ internal class DefaultAddressBookRepository( override fun getContacts(userWalletId: UserWalletId): Flow> { return getContactsForWallet(userWalletId) - .onStart { syncAddressBooks() } .distinctUntilChanged() .flowOn(dispatchers.default) } @@ -70,7 +69,6 @@ internal class DefaultAddressBookRepository( } } } - .onStart { syncAddressBooks() } .distinctUntilChanged() .flowOn(dispatchers.default) } diff --git a/data/address-book/src/test/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepositoryTest.kt b/data/address-book/src/test/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepositoryTest.kt index 079d762fe6..732cb1d8b9 100644 --- a/data/address-book/src/test/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepositoryTest.kt +++ b/data/address-book/src/test/kotlin/com/tangem/data/addressbook/DefaultAddressBookRepositoryTest.kt @@ -27,7 +27,6 @@ import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -88,7 +87,7 @@ internal class DefaultAddressBookRepositoryTest { } @Test - fun `GIVEN blob WHEN getContacts THEN syncs before reading contacts`() = runTest { + fun `GIVEN blob WHEN getContacts THEN does not sync on collection`() = runTest { // Arrange val contact = createContact(id = "c1", name = "Alice") val blob = createBlob() @@ -99,10 +98,8 @@ internal class DefaultAddressBookRepositoryTest { repository.getContacts(UserWalletId(WALLET_A)).first() // Assert - coVerifyOrder { - addressBookApi.syncAddressBooks(any()) - cipher.decrypt(blob, userWallet) - } + // Sync is now triggered by the feature entry points, not on flow collection. + coVerify(exactly = 0) { addressBookApi.syncAddressBooks(any()) } } @Test @@ -122,7 +119,7 @@ internal class DefaultAddressBookRepositoryTest { } @Test - fun `GIVEN blob WHEN getAllContacts THEN syncs before reading contacts`() = runTest { + fun `GIVEN blob WHEN getAllContacts THEN does not sync on collection`() = runTest { // Arrange val contact = createContact(id = "c1", name = "Alice") val blob = createBlob() @@ -134,10 +131,8 @@ internal class DefaultAddressBookRepositoryTest { repository.getAllContacts().first() // Assert - coVerifyOrder { - addressBookApi.syncAddressBooks(any()) - cipher.decrypt(blob, userWallet) - } + // Sync is now triggered by the feature entry points, not on flow collection. + coVerify(exactly = 0) { addressBookApi.syncAddressBooks(any()) } } @Test diff --git a/domain/address-book/build.gradle.kts b/domain/address-book/build.gradle.kts index a422d47b73..79b86b8be8 100644 --- a/domain/address-book/build.gradle.kts +++ b/domain/address-book/build.gradle.kts @@ -25,6 +25,10 @@ dependencies { implementation(projects.core.utils) // endregion + // region Libs + implementation(projects.libs.crypto) + // endregion + // region Domain api(projects.domain.common) api(projects.domain.tokens) diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt index d7cd9b2bb3..e85ba5940b 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.usecase import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.repository.AddressBookRepository import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.lib.crypto.BlockchainUtils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -30,8 +31,14 @@ class GetContactsUseCase( private fun Contact.matches(query: String): Boolean { val isNameContaining = name.value.contains(other = query, ignoreCase = true) 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) } - return isNameContaining || isAddressContaining + val isNetworkContaining = addresses.any { addressEntry -> + addressEntry.networkId.value.contains(other = query, ignoreCase = true) + } + return isNameContaining || isAddressContaining || isNetworkContaining } } \ No newline at end of file diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt index fbe1f2d4a2..d5a26d4a10 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt @@ -54,31 +54,71 @@ class GetContactsUseCaseTest { } @Test - fun `GIVEN address query matching case WHEN invoke THEN returns matching contact`() = runTest { - // Arrange - val carol = contact(name = "Carol", address = "0xAbCdEf") - 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") + fun `GIVEN EVM address query differing only in case WHEN invoke THEN returns matching contact`() = runTest { + // Arrange — EVM (ethereum) addresses are case-insensitive, so a lowercased query matches a checksummed address + 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 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 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 + fun `GIVEN query matches a network id WHEN invoke THEN returns only contacts on that network`() = runTest { + // Arrange + val ethContact = contact(name = "Eth", address = "0x1", networkId = "ethereum") + val tronContact = contact(name = "Tron", address = "T1", networkId = "tron") + every { repository.getAllContacts() } returns flowOf(listOf(ethContact, tronContact)) + + // Act + val result = useCase(query = "tron").first() + + // Assert + assertThat(result).containsExactly(tronContact) + } + + @Test + fun `GIVEN network query with different case WHEN invoke THEN returns matching contact`() = runTest { + // Arrange + val tronContact = contact(name = "Tron", address = "T1", networkId = "tron") + every { repository.getAllContacts() } returns flowOf(listOf(alice, tronContact)) + + // Act + val result = useCase(query = "TRON").first() + + // Assert + assertThat(result).containsExactly(tronContact) + } + @Test fun `GIVEN name query with different case WHEN invoke THEN returns matching contact`() = runTest { // Act @@ -139,6 +179,7 @@ class GetContactsUseCaseTest { name: String, address: String, createdAt: String = "2026-01-01T00:00:00.000Z", + networkId: String = "ethereum", ): Contact = Contact( id = ContactId("id-$name"), walletId = UserWalletId("011"), @@ -151,7 +192,7 @@ class GetContactsUseCaseTest { AddressEntry( id = AddressEntryId("addr-$name"), address = address, - networkId = Network.RawID("ethereum"), + networkId = Network.RawID(networkId), memo = null, signature = "sig", ), diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt index 88648c1649..f5c36a478b 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.addressbook.usecase.GetContactsUseCase +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.addressbook.AddressBookContactsBlockComponent import com.tangem.features.addressbook.MatchedContact @@ -15,15 +16,18 @@ import com.tangem.features.addressbook.common.ContactMatcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @ModelScoped +@Suppress("LongParameterList") internal class ContactsBlockModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val stateController: ContactsBlockStateController, private val analyticsSender: AddressBookAnalyticsSender, + private val syncAddressBooksUseCase: SyncAddressBooksUseCase, getContactsUseCase: GetContactsUseCase, getWalletsUseCase: GetWalletsUseCase, ) : Model() { @@ -33,6 +37,8 @@ internal class ContactsBlockModel @Inject constructor( val state: StateFlow get() = stateController.uiState init { + modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() } + combine( params.queryFlow.flatMapLatest { query -> getContactsUseCase(query = query, userWalletId = null) diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt index 735b5798b6..995ebd9b60 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt @@ -71,7 +71,7 @@ internal class DefaultAddressBookListComponent( /** * @property mode Default (management) or Selector (pick a contact for a network) - * @property onContactClick management mode — opens the contact editor (TODO [REDACTED_TASK_KEY]) + * @property onContactClick management mode — opens the contact editor * @property onAddContactClick opens the new-contact editor */ data class Params( diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt index 80625ad2e7..7203095b1b 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt @@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor import com.tangem.domain.addressbook.model.VerifiedContact +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -26,12 +27,13 @@ import com.tangem.features.addressbook.route.AddressBookRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject /** * Backs the contacts list. The list content is the same however the address book was opened — the open * [AddressBookRoute.ListMode] only decides what tapping a contact does: - * - [AddressBookRoute.ListMode.Default]: browse / manage contacts (editor is TODO [REDACTED_TASK_KEY]). + * - [AddressBookRoute.ListMode.Default]: browse / manage contacts * - [AddressBookRoute.ListMode.Selector]: pick a recipient for the given network — a single matching address is * returned right away, several open the address selector first. */ @@ -45,8 +47,9 @@ internal class AddressBookListModel @Inject constructor( private val router: Router, private val contactSelectionTrigger: ContactSelectionTrigger, private val analyticsSender: AddressBookAnalyticsSender, - getVerifiedContactsInteractor: GetVerifiedContactsInteractor, - getWalletsUseCase: GetWalletsUseCase, + private val syncAddressBooksUseCase: SyncAddressBooksUseCase, + private val getVerifiedContactsInteractor: GetVerifiedContactsInteractor, + private val getWalletsUseCase: GetWalletsUseCase, ) : Model() { private val params = paramsContainer.require() @@ -65,6 +68,14 @@ internal class AddressBookListModel @Inject constructor( .shareIn(modelScope, SharingStarted.Lazily, replay = 1) init { + modelScope.launch { + syncAddressBooksUseCase() + sendContactListScreenOpenedEvent() + observeContacts() + } + } + + private suspend fun observeContacts() { val matchedContacts = searchQuery.flatMapLatest { query -> if (query.isBlank()) { allContacts @@ -89,9 +100,7 @@ internal class AddressBookListModel @Inject constructor( } .onEach(::updateState) .flowOn(dispatchers.default) - .launchIn(modelScope) - - sendContactListScreenOpenedEvent() + .collect() } fun deliverSelection(contact: SelectedContact) { @@ -155,17 +164,13 @@ internal class AddressBookListModel @Inject constructor( } } - private fun sendContactListScreenOpenedEvent() { - allContacts - .take(count = 1) - .onEach { contacts -> - analyticsSender.sendContactListScreenOpened( - source = params.mode.toAnalyticsSource(), - contactsCount = contacts.size, - scope = modelScope, - ) - } - .launchIn(modelScope) + private suspend fun sendContactListScreenOpenedEvent() { + val contacts = allContacts.first() + analyticsSender.sendContactListScreenOpened( + source = params.mode.toAnalyticsSource(), + contactsCount = contacts.size, + scope = modelScope, + ) } private fun AddressBookRoute.ListMode.toAnalyticsSource(): Source = when (this) { diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt index 0dc7ed6959..1f7e03f325 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import com.tangem.core.decompose.model.MutableParamsContainer import com.tangem.domain.addressbook.model.* import com.tangem.domain.addressbook.usecase.GetContactsUseCase +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -37,6 +38,7 @@ internal class ContactsBlockModelTest { private val getContactsUseCase: GetContactsUseCase = mockk() private val getWalletsUseCase: GetWalletsUseCase = mockk() + private val syncAddressBooksUseCase: SyncAddressBooksUseCase = mockk(relaxed = true) private val analyticsSender: AddressBookAnalyticsSender = mockk(relaxed = true) private val network: Network = mockk { every { rawId } returns ETHEREUM } @@ -134,6 +136,7 @@ internal class ContactsBlockModelTest { dispatchers = testScope.createTestingCoroutineDispatcherProvider(), stateController = ContactsBlockStateController(), analyticsSender = analyticsSender, + syncAddressBooksUseCase = syncAddressBooksUseCase, getContactsUseCase = getContactsUseCase, getWalletsUseCase = getWalletsUseCase, ).also { model = it } diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt index 16ff3913ec..53294d8d38 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt @@ -1,13 +1,14 @@ package com.tangem.features.addressbook.list.model +import arrow.core.right import com.google.common.truth.Truth.assertThat import com.tangem.core.decompose.model.MutableParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor import com.tangem.domain.addressbook.model.* +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.addressbook.ContactSelectionTrigger @@ -20,9 +21,11 @@ import com.tangem.features.addressbook.list.ui.state.ContentMode import com.tangem.features.addressbook.route.AddressBookRoute import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -44,14 +47,21 @@ internal class AddressBookListModelTest { private val analyticsSender: AddressBookAnalyticsSender = mockk(relaxed = true) private val getVerifiedContactsInteractor: GetVerifiedContactsInteractor = mockk() private val getWalletsUseCase: GetWalletsUseCase = mockk() + private val syncAddressBooksUseCase: SyncAddressBooksUseCase = mockk(relaxed = true) private var model: AddressBookListModel? = null @BeforeEach fun resetMocks() { - clearMocks(getVerifiedContactsInteractor, getWalletsUseCase, analyticsSender, contactSelectionTrigger) + clearMocks( + getVerifiedContactsInteractor, + getWalletsUseCase, + analyticsSender, + contactSelectionTrigger, + syncAddressBooksUseCase, + ) every { getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true) } returns - flowOf(linkedMapOf()) + flowOf(linkedMapOf()) } @AfterEach @@ -72,6 +82,29 @@ internal class AddressBookListModelTest { assertThat(model.state.value).isEqualTo(AddressBookListUM.Loading) } + @Test + fun `GIVEN cached contacts AND sync in progress WHEN created THEN stays Loading until sync completes`() = runTest { + // Arrange — contacts are already cached locally, but the open-time sync has not returned yet. + val syncGate = CompletableDeferred() + coEvery { syncAddressBooksUseCase() } coAnswers { syncGate.await(); Unit.right() } + every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns + flowOf(listOf(verifiedContact(id = "1", name = "Alice"))) + + // Act + val model = createModel(testScope = this, mode = AddressBookRoute.ListMode.Default) + advanceUntilIdle() + + // Assert — the possibly-stale cache is not revealed while the sync is still running. + assertThat(model.state.value).isEqualTo(AddressBookListUM.Loading) + + // Act — the sync finishes. + syncGate.complete(Unit) + advanceUntilIdle() + + // Assert — the list is revealed only after the sync completed. + assertThat(model.state.value).isInstanceOf(AddressBookListUM.Content::class.java) + } + @Test fun `GIVEN default mode AND verified contacts WHEN created THEN content shown`() = runTest { // Arrange @@ -212,6 +245,7 @@ internal class AddressBookListModelTest { router = router, contactSelectionTrigger = contactSelectionTrigger, analyticsSender = analyticsSender, + syncAddressBooksUseCase = syncAddressBooksUseCase, getVerifiedContactsInteractor = getVerifiedContactsInteractor, getWalletsUseCase = getWalletsUseCase, ).also { model = it } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt index df4f3b2b1c..74f6520ad9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAdd import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.usecase.GetContactsUseCase +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue @@ -37,6 +38,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.addressbook.AddressBookFeatureToggles import com.tangem.features.addressbook.AddressBookSendAnalytics import com.tangem.features.addressbook.ContactSelectionListener import com.tangem.features.addressbook.MatchedContact @@ -88,6 +90,8 @@ internal class SendDestinationModel @Inject constructor( private val sendDestinationAlertFactory: SendDestinationAlertFactory, private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val addressBookSendAnalytics: AddressBookSendAnalytics, + private val syncAddressBooksUseCase: SyncAddressBooksUseCase, + private val addressBookFeatureToggles: AddressBookFeatureToggles, getContactsUseCase: GetContactsUseCase, contactSelectionListener: ContactSelectionListener, ) : Model(), SendDestinationClickIntents { @@ -139,6 +143,7 @@ internal class SendDestinationModel @Inject constructor( private val backupProblematicWalletCache = AtomicReference?>(null) init { + syncAddressBooksIfNeeded() subscribeOnQRScannerResult() initialState() resetContactOnEdit() @@ -286,6 +291,12 @@ internal class SendDestinationModel @Inject constructor( }.launchIn(modelScope) } + private fun syncAddressBooksIfNeeded() { + if (addressBookFeatureToggles.isAddressBookEnabled) { + modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() } + } + } + private fun subscribeOnQRScannerResult() { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } diff --git a/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt b/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt index 58a5da71fc..8c81544ec3 100644 --- a/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt +++ b/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt @@ -16,6 +16,7 @@ import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAdd import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.addressbook.model.* import com.tangem.domain.addressbook.usecase.GetContactsUseCase +import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.feedback.SendBackupProblemEmailUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress @@ -33,6 +34,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.addressbook.AddressBookFeatureToggles import com.tangem.features.addressbook.AddressBookSendAnalytics import com.tangem.features.addressbook.ContactSelectionListener import com.tangem.features.addressbook.MatchedContact @@ -93,6 +95,8 @@ internal class SendDestinationModelTest { private val sendDestinationAlertFactory: SendDestinationAlertFactory = mockk(relaxed = true) private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true) private val getContactsUseCase: GetContactsUseCase = 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 addressBookSendAnalytics: AddressBookSendAnalytics = mockk(relaxed = true) private val callback: SendDestinationComponent.ModelCallback = mockk(relaxed = true) @@ -513,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 private fun TestScope.buildModel( @@ -571,6 +605,8 @@ internal class SendDestinationModelTest { sendDestinationAlertFactory = sendDestinationAlertFactory, sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase, addressBookSendAnalytics = addressBookSendAnalytics, + syncAddressBooksUseCase = syncAddressBooksUseCase, + addressBookFeatureToggles = addressBookFeatureToggles, getContactsUseCase = getContactsUseCase, contactSelectionListener = contactSelectionListener, )