From e2a7b906587356030fd7ccc6ec1006125c6011e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 18:04:23 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../analytics/AddressBookEvents.kt | 4 + .../common/AddressBookAnalyticsSender.kt | 112 +++++++++++ .../common/DefaultAddressBookComponent.kt | 3 + .../editcontact/model/EditContactModel.kt | 28 ++- .../common/AddressBookAnalyticsSenderTest.kt | 182 ++++++++++++++++++ .../editcontact/model/EditContactModelTest.kt | 145 +++++++++++++- 6 files changed, 471 insertions(+), 3 deletions(-) create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSender.kt create mode 100644 features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSenderTest.kt diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/analytics/AddressBookEvents.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/analytics/AddressBookEvents.kt index 6597892bf4..d371791ca2 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/analytics/AddressBookEvents.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/analytics/AddressBookEvents.kt @@ -17,6 +17,10 @@ sealed class AddressBookEvents( ) : AnalyticsEvent(ADDRESS_BOOK_CATEGORY, event, params) { // region Contact creation + data object SaveToButtonClicked : AddressBookEvents(event = "Button - Save To") + + data object AddressScreenOpened : AddressBookEvents(event = "Address Screen Opened") + class ContactListScreenOpened( walletId: UserWalletId, source: Source, diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSender.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSender.kt new file mode 100644 index 0000000000..5bf6ce299e --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSender.kt @@ -0,0 +1,112 @@ +package com.tangem.features.addressbook.common + +import com.tangem.common.routing.entity.AddressBookOpenMode +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.addressbook.error.AddressBookSyncError +import com.tangem.domain.addressbook.error.SaveContactError +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.addressbook.analytics.AddressBookEvents +import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactListScreenOpened.Source +import com.tangem.features.addressbook.analytics.AddressBookEvents.SaveErrorShown.ErrorType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class AddressBookAnalyticsSender @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, + private val userWalletsListRepository: UserWalletsListRepository, +) { + + fun sendContactListScreenOpened(mode: AddressBookOpenMode, scope: CoroutineScope) { + scope.launch { + analyticsEventHandler.send( + AddressBookEvents.ContactListScreenOpened( + walletId = selectedWalletId(), + source = mode.toAnalyticsSource(), + ), + ) + } + } + + fun sendAddContactTapped(fromSendSuccess: Boolean, scope: CoroutineScope) { + scope.launch { + analyticsEventHandler.send( + AddressBookEvents.AddContactTapped( + walletId = selectedWalletId(), + source = if (fromSendSuccess) { + AddressBookEvents.AddContactTapped.Source.SendSuccess + } else { + AddressBookEvents.AddContactTapped.Source.Settings + }, + ), + ) + } + } + + fun sendContactSaved(walletId: UserWalletId, contactId: String, isEdit: Boolean) { + analyticsEventHandler.send( + AddressBookEvents.ContactSaved( + walletId = walletId, + contactId = contactId, + mode = if (isEdit) { + AddressBookEvents.ContactSaved.Mode.Edit + } else { + AddressBookEvents.ContactSaved.Mode.Create + }, + ), + ) + } + + /** + * Fired when a save failure is surfaced to the user. [contactId] is set only in edit mode. Validation failures + * ([SaveContactError.Name]/[SaveContactError.Address]) are shown inline rather than as a save error, so they do + * not produce this event. + */ + fun sendSaveErrorShown(walletId: UserWalletId, contactId: String?, error: SaveContactError) { + val errorType = error.toErrorType() ?: return + analyticsEventHandler.send( + AddressBookEvents.SaveErrorShown( + walletId = walletId, + contactId = contactId, + errorType = errorType, + ), + ) + } + + private fun SaveContactError.toErrorType(): ErrorType? = when (this) { + is SaveContactError.Signing -> ErrorType.Signing + // Network means the backend was unreachable; every other backend outcome (5xx, 412, other codes) is Server. + is SaveContactError.Backend -> when (error) { + AddressBookSyncError.Network -> ErrorType.Network + else -> ErrorType.Server + } + is SaveContactError.Name, + is SaveContactError.Address, + -> null + } + + fun sendSaveToButtonClicked() { + analyticsEventHandler.send(AddressBookEvents.SaveToButtonClicked) + } + + fun sendAddressScreenOpened() { + analyticsEventHandler.send(AddressBookEvents.AddressScreenOpened) + } + + private suspend fun selectedWalletId(): UserWalletId = userWalletsListRepository.selectedUserWallet + .filterNotNull() + .first() + .walletId + + private fun AddressBookOpenMode.toAnalyticsSource(): Source = when (this) { + AddressBookOpenMode.Default -> Source.Settings + is AddressBookOpenMode.ContactSelection, + is AddressBookOpenMode.WithContactCreation, + -> Source.SendFlow + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt index 3e216c7bb1..8dc6aa6ecc 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt @@ -30,6 +30,7 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( private val childFactory: AddressBookChildFactory, private val resultHolder: AddressBookResultHolder, private val selectNetworksResultHolder: SelectNetworksResultHolder, + private val analyticsSender: AddressBookAnalyticsSender, ) : AddressBookComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -38,6 +39,8 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( // Drop any results left over from a previous session before the (possibly preloaded) stack starts collecting. resultHolder.clear() selectNetworksResultHolder.clear() + + analyticsSender.sendContactListScreenOpened(mode = params.addressBookOpenMode, scope = componentScope) } private val clickIntents = object : AddressBookClickIntents { diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt index cdcdb538ff..b311624f37 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked +import com.tangem.features.addressbook.common.AddressBookAnalyticsSender import com.tangem.features.addressbook.common.AddressBookResultHolder import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent import com.tangem.features.addressbook.editcontact.state.EditContactStateController @@ -50,6 +51,7 @@ internal class EditContactModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val validateContactNameUseCase: ValidateContactNameUseCase, private val saveContactInteractor: SaveContactInteractor, + private val analyticsSender: AddressBookAnalyticsSender, val portfolioSelectorController: PortfolioSelectorController, portfolioFetcherFactory: PortfolioFetcher.Factory, ) : Model() { @@ -86,6 +88,12 @@ internal class EditContactModel @Inject constructor( observeWalletBlock() observeNameValidation() observeSaveButton() + sendAddContactTappedEvent() + } + + private fun sendAddContactTappedEvent() { + if (params.contactId != null) return + analyticsSender.sendAddContactTapped(fromSendSuccess = params.predefinedAddress != null, scope = modelScope) } /** In WithContactCreation mode the contact opens with the already-known address attached. */ @@ -151,6 +159,7 @@ internal class EditContactModel @Inject constructor( private fun onWalletBlockClick() { if (isWalletChangeable(userWalletsListRepository.userWallets.value)) { + analyticsSender.sendSaveToButtonClicked() portfolioSelectorNavigation.activate(Unit) } } @@ -214,8 +223,22 @@ internal class EditContactModel @Inject constructor( addressEntries = addressEntries, ) result.fold( - ifLeft = ::handleSaveError, - ifRight = { params.onBackClick() }, + ifLeft = { error -> + handleSaveError(error) + analyticsSender.sendSaveErrorShown( + walletId = userWallet.walletId, + contactId = params.contactId?.value, + error = error, + ) + }, + ifRight = { contact -> + analyticsSender.sendContactSaved( + walletId = userWallet.walletId, + contactId = contact.id.value, + isEdit = params.contactId != null, + ) + params.onBackClick() + }, ) } finally { refreshSaveButton() @@ -247,6 +270,7 @@ internal class EditContactModel @Inject constructor( ), ) } else { + analyticsSender.sendAddressScreenOpened() params.onAddAddressClick() } } diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSenderTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSenderTest.kt new file mode 100644 index 0000000000..913bbf771c --- /dev/null +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/common/AddressBookAnalyticsSenderTest.kt @@ -0,0 +1,182 @@ +package com.tangem.features.addressbook.common + +import com.tangem.common.routing.entity.AddressBookOpenMode +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.addressbook.error.AddressBookSyncError +import com.tangem.domain.addressbook.error.SaveContactError +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.addressbook.analytics.AddressBookEvents +import com.tangem.features.addressbook.analytics.AddressBookEvents.AddContactTapped +import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactListScreenOpened.Source +import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactSaved +import com.tangem.features.addressbook.analytics.AddressBookEvents.SaveErrorShown.ErrorType +import com.tangem.test.core.ProvideTestModels +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class AddressBookAnalyticsSenderTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk() + + private val sender = AddressBookAnalyticsSender( + analyticsEventHandler = analyticsEventHandler, + userWalletsListRepository = userWalletsListRepository, + ) + + @BeforeEach + fun setup() { + clearMocks(analyticsEventHandler) + val wallet = mockk { every { walletId } returns EXPECTED_WALLET_ID } + every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet) + } + + @ParameterizedTest + @ProvideTestModels + fun sendContactListScreenOpened(model: ScreenOpenedModel) = runTest { + // Act + sender.sendContactListScreenOpened(mode = model.mode, scope = this) + advanceUntilIdle() + + // Assert + val expected = AddressBookEvents.ContactListScreenOpened( + walletId = EXPECTED_WALLET_ID, + source = model.expectedSource, + ) + verify(exactly = 1) { analyticsEventHandler.send(expected) } + } + + @ParameterizedTest + @MethodSource("provideAddContactModels") + fun sendAddContactTapped(model: AddContactModel) = runTest { + // Act + sender.sendAddContactTapped(fromSendSuccess = model.fromSendSuccess, scope = this) + advanceUntilIdle() + + // Assert + val expected = AddressBookEvents.AddContactTapped( + walletId = EXPECTED_WALLET_ID, + source = model.expectedSource, + ) + verify(exactly = 1) { analyticsEventHandler.send(expected) } + } + + @ParameterizedTest + @MethodSource("provideContactSavedModels") + fun sendContactSaved(model: ContactSavedModel) = runTest { + // Act + sender.sendContactSaved(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID, isEdit = model.isEdit) + + // Assert + val expected = AddressBookEvents.ContactSaved( + walletId = EXPECTED_WALLET_ID, + contactId = CONTACT_ID, + mode = model.expectedMode, + ) + verify(exactly = 1) { analyticsEventHandler.send(expected) } + } + + @ParameterizedTest + @MethodSource("provideSaveErrorModels") + fun sendSaveErrorShown(model: SaveErrorModel) = runTest { + // Act + sender.sendSaveErrorShown(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID, error = model.error) + + // Assert + val expectedType = model.expectedType + if (expectedType == null) { + verify(exactly = 0) { analyticsEventHandler.send(any()) } + } else { + val expected = AddressBookEvents.SaveErrorShown( + walletId = EXPECTED_WALLET_ID, + contactId = CONTACT_ID, + errorType = expectedType, + ) + verify(exactly = 1) { analyticsEventHandler.send(expected) } + } + } + + @Test + fun `WHEN sendSaveToButtonClicked THEN event sent`() { + // Act + sender.sendSaveToButtonClicked() + + // Assert + verify(exactly = 1) { analyticsEventHandler.send(AddressBookEvents.SaveToButtonClicked) } + } + + @Test + fun `WHEN sendAddressScreenOpened THEN event sent`() { + // Act + sender.sendAddressScreenOpened() + + // Assert + verify(exactly = 1) { analyticsEventHandler.send(AddressBookEvents.AddressScreenOpened) } + } + + internal data class ScreenOpenedModel(val mode: AddressBookOpenMode, val expectedSource: Source) + + internal data class AddContactModel(val fromSendSuccess: Boolean, val expectedSource: AddContactTapped.Source) + + internal data class ContactSavedModel(val isEdit: Boolean, val expectedMode: ContactSaved.Mode) + + internal data class SaveErrorModel(val error: SaveContactError, val expectedType: ErrorType?) + + private fun provideTestModels() = listOf( + ScreenOpenedModel(mode = AddressBookOpenMode.Default, expectedSource = Source.Settings), + ScreenOpenedModel( + mode = AddressBookOpenMode.ContactSelection(networkId = "ethereum"), + expectedSource = Source.SendFlow, + ), + ScreenOpenedModel( + mode = AddressBookOpenMode.WithContactCreation(address = "0xABC", networkId = "ethereum"), + expectedSource = Source.SendFlow, + ), + ) + + private fun provideAddContactModels() = listOf( + AddContactModel(fromSendSuccess = false, expectedSource = AddContactTapped.Source.Settings), + AddContactModel(fromSendSuccess = true, expectedSource = AddContactTapped.Source.SendSuccess), + ) + + private fun provideContactSavedModels() = listOf( + ContactSavedModel(isEdit = false, expectedMode = ContactSaved.Mode.Create), + ContactSavedModel(isEdit = true, expectedMode = ContactSaved.Mode.Edit), + ) + + private fun provideSaveErrorModels() = listOf( + SaveErrorModel(error = SaveContactError.Signing(mockk()), expectedType = ErrorType.Signing), + SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Network), expectedType = ErrorType.Network), + // 412 + SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Conflict), expectedType = ErrorType.Server), + // 5xx / unmapped + SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Unknown), expectedType = ErrorType.Server), + SaveErrorModel( + error = SaveContactError.Backend(AddressBookSyncError.BadRequest), + expectedType = ErrorType.Server, + ), + // Validation failures are shown inline, not as a save error. + SaveErrorModel(error = SaveContactError.Name(mockk()), expectedType = null), + SaveErrorModel(error = SaveContactError.Address(mockk()), expectedType = null), + ) + + private companion object { + val EXPECTED_WALLET_ID = UserWalletId("0011223344") + const val CONTACT_ID = "contact-42" + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt index 40b59525d2..111e887f3b 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.addressbook.error.AddressBookSyncError import com.tangem.domain.addressbook.error.ContactNameValidationError import com.tangem.domain.addressbook.error.SaveContactError import com.tangem.domain.addressbook.interactor.SaveContactInteractor @@ -23,6 +24,7 @@ import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.addressbook.common.AddressBookAnalyticsSender import com.tangem.features.addressbook.common.AddressBookResultHolder import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent import com.tangem.features.addressbook.editcontact.state.EditContactStateController @@ -60,6 +62,7 @@ internal class EditContactModelTest { private val portfolioSelectorController: PortfolioSelectorController = mockk() private val portfolioFetcher: PortfolioFetcher = mockk(relaxed = true) private val portfolioFetcherFactory: PortfolioFetcher.Factory = mockk() + private val analyticsSender: AddressBookAnalyticsSender = mockk(relaxed = true) // Drives the wallet picked in the reused portfolio selector; `first` of the pair is the chosen wallet. private val selectedWalletData = @@ -121,6 +124,41 @@ internal class EditContactModelTest { assertThat(state).isEqualTo(expected) } + @Test + fun `GIVEN new contact without predefined address WHEN created THEN AddContactTapped sent from settings`() = + runTest { + // Act + createModel(testScope = this, params = createParams(contactId = null, predefinedAddress = null)) + advanceUntilIdle() + + // Assert + verify(exactly = 1) { analyticsSender.sendAddContactTapped(fromSendSuccess = false, scope = any()) } + } + + @Test + fun `GIVEN new contact with predefined address WHEN created THEN AddContactTapped sent from send success`() = + runTest { + // Arrange + val predefined = ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum")) + + // Act + createModel(testScope = this, params = createParams(predefinedAddress = predefined)) + advanceUntilIdle() + + // Assert + verify(exactly = 1) { analyticsSender.sendAddContactTapped(fromSendSuccess = true, scope = any()) } + } + + @Test + fun `GIVEN existing contactId WHEN model created THEN AddContactTapped not sent`() = runTest { + // Act + createModel(testScope = this, params = createParams(contactId = ContactId(value = "contact-id"))) + advanceUntilIdle() + + // Assert + verify(exactly = 0) { analyticsSender.sendAddContactTapped(any(), any()) } + } + @Test fun `GIVEN existing contactId WHEN model created THEN title is contact`() = runTest { // Arrange @@ -216,6 +254,7 @@ internal class EditContactModelTest { // Assert assertThat(addClicked).isTrue() verify(exactly = 0) { messageSender.send(any()) } + verify(exactly = 1) { analyticsSender.sendAddressScreenOpened() } } @Test @@ -240,8 +279,40 @@ internal class EditContactModelTest { assertThat(model.state.value.isAddAddressEnabled).isFalse() assertThat(addClicked).isFalse() verify { messageSender.send(any()) } + verify(exactly = 0) { analyticsSender.sendAddressScreenOpened() } } + @Test + fun `GIVEN changeable wallet block WHEN clicked THEN SaveToButtonClicked sent`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + val walletB = createWallet(id = "bb", name = "Wallet B") + setupWallets(wallets = listOf(walletA, walletB), selected = walletA) + val model = createModel(testScope = this) + advanceUntilIdle() + + // Act + model.state.value.walletBlock.onClick() + + // Assert + verify(exactly = 1) { analyticsSender.sendSaveToButtonClicked() } + } + + @Test + fun `GIVEN non-changeable wallet block WHEN clicked THEN SaveToButtonClicked not sent`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + val model = createModel(testScope = this) + advanceUntilIdle() + + // Act + model.state.value.walletBlock.onClick() + + // Assert + verify(exactly = 0) { analyticsSender.sendSaveToButtonClicked() } + } + @Test fun `GIVEN new contact AND multiple unlocked wallets WHEN created THEN wallet block changeable`() = runTest { // Arrange @@ -391,7 +462,8 @@ internal class EditContactModelTest { var navigatedBack = false val walletA = createWallet(id = "aa", name = "Wallet A") setupWallets(wallets = listOf(walletA), selected = walletA) - coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns mockk().right() + val savedContact = mockk { every { id } returns ContactId(value = "saved-1") } + coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns savedContact.right() val model = createModel(testScope = this, params = createParams(onBackClick = { navigatedBack = true })) advanceUntilIdle() model.state.value.onNameChange("Satoshi") @@ -411,9 +483,79 @@ internal class EditContactModelTest { addressEntries = any(), ) } + verify(exactly = 1) { + analyticsSender.sendContactSaved(walletId = walletA.walletId, contactId = "saved-1", isEdit = false) + } assertThat(navigatedBack).isTrue() } + @Test + fun `GIVEN save fails WHEN save clicked THEN ContactSaved not sent`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns + SaveContactError.Name(ContactNameValidationError.Duplicate).left() + val model = createModel(testScope = this) + advanceUntilIdle() + model.state.value.onNameChange("Satoshi") + resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))) + advanceUntilIdle() + + // Act + model.state.value.saveButton.onClick() + advanceUntilIdle() + + // Assert + verify(exactly = 0) { analyticsSender.sendContactSaved(any(), any(), any()) } + } + + @Test + fun `GIVEN new contact AND save fails WHEN save clicked THEN SaveErrorShown sent with null contactId`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + val error = SaveContactError.Backend(AddressBookSyncError.Network) + coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns error.left() + val model = createModel(testScope = this, params = createParams(contactId = null)) + advanceUntilIdle() + model.state.value.onNameChange("Satoshi") + resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))) + advanceUntilIdle() + + // Act + model.state.value.saveButton.onClick() + advanceUntilIdle() + + // Assert + verify(exactly = 1) { + analyticsSender.sendSaveErrorShown(walletId = walletA.walletId, contactId = null, error = error) + } + } + + @Test + fun `GIVEN existing contact AND save fails WHEN save clicked THEN SaveErrorShown sent with contactId`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + val error = SaveContactError.Backend(AddressBookSyncError.Network) + coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns error.left() + val model = createModel(testScope = this, params = createParams(contactId = ContactId(value = "contact-id"))) + advanceUntilIdle() + model.state.value.onNameChange("Satoshi") + resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))) + advanceUntilIdle() + + // Act + model.state.value.saveButton.onClick() + advanceUntilIdle() + + // Assert + verify(exactly = 1) { + analyticsSender.sendSaveErrorShown(walletId = walletA.walletId, contactId = "contact-id", error = error) + } + } + @Test fun `GIVEN save returns name error WHEN save clicked THEN inline name error shown`() = runTest { // Arrange @@ -462,6 +604,7 @@ internal class EditContactModelTest { userWalletsListRepository = userWalletsListRepository, validateContactNameUseCase = validateContactNameUseCase, saveContactInteractor = saveContactInteractor, + analyticsSender = analyticsSender, portfolioSelectorController = portfolioSelectorController, portfolioFetcherFactory = portfolioFetcherFactory, ).also { model = it }