diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 215563541d..b54a4ae79a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -105,6 +105,7 @@ %d addresses Choose address + Clear All Contact Contact name Copy address @@ -134,6 +135,7 @@ Save to Wallet This contact will be linked to this wallet’s address book. No results found.\nTry another name + Select All Select network Address book Unsaved changes 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 9a1448a028..e11a6468fe 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 @@ -11,6 +11,7 @@ private const val WALLET_ID = "Wallet Id" private const val CONTACT_ID = "Contact Id" private const val MODE = "Mode" private const val CONTACTS_COUNT = "Contacts Count" +private const val ACTION = "Action" sealed class AddressBookEvents( event: String, @@ -162,6 +163,24 @@ sealed class AddressBookEvents( ) // endregion + // region Choose network + class SelectAllNetworksTapped( + walletId: UserWalletId, + action: Action, + ) : AddressBookEvents( + event = "Select All Networks Tapped", + params = mapOf( + WALLET_ID to walletId.stringValue, + ACTION to action.value, + ), + ) { + enum class Action(val value: String) { + SelectAll("Select All"), + ClearAll("Clear All"), + } + } + // endregion + // region Deletion class AddressRemoved( walletId: UserWalletId, 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 index 591d502920..e43c1a4813 100644 --- 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 @@ -88,6 +88,14 @@ internal class AddressBookAnalyticsSender @Inject constructor( ) } + fun sendSelectAllNetworksTapped(action: AddressBookEvents.SelectAllNetworksTapped.Action, scope: CoroutineScope) { + scope.launch(dispatcherProvider.default) { + analyticsEventHandler.send( + AddressBookEvents.SelectAllNetworksTapped(walletId = selectedWalletId(), action = action), + ) + } + } + fun sendAddContactTapped(fromSendSuccess: Boolean, scope: CoroutineScope) { scope.launch(dispatcherProvider.default) { analyticsEventHandler.send( diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModel.kt index c91befa6e9..3c235410f0 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModel.kt @@ -6,6 +6,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.addressbook.analytics.AddressBookEvents +import com.tangem.features.addressbook.common.AddressBookAnalyticsSender import com.tangem.features.addressbook.selectnetworks.DefaultSelectNetworksComponent import com.tangem.features.addressbook.selectnetworks.state.SelectNetworksStateController import com.tangem.features.addressbook.selectnetworks.state.transformers.UpdateNetworksContentTransformer @@ -22,6 +24,7 @@ internal class SelectNetworksModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val stateController: SelectNetworksStateController, + private val analyticsSender: AddressBookAnalyticsSender, ) : Model() { private val params: DefaultSelectNetworksComponent.Params = paramsContainer.require() @@ -50,16 +53,18 @@ internal class SelectNetworksModel @Inject constructor( onActiveChange = ::onActiveChange, onBackClick = params.onBackClick, onDoneClick = ::onDoneClick, + onSelectAllClick = ::onSelectAllClick, ), ) } private fun subscribeToContent() { - combine(query, selectedNetworks) { query, selection -> + combine(query, selectedNetworks, isSearchActive) { query, selection, isSearchActive -> UpdateNetworksContentTransformer( matchedBlockchains = matchedBlockchains, query = query, selectedNetworkIds = selection, + isSearchActive = isSearchActive, onToggle = ::onToggle, ) } @@ -88,6 +93,22 @@ internal class SelectNetworksModel @Inject constructor( selectedNetworks.value = if (networkId in current) current - networkId else current + networkId } + private fun onSelectAllClick() { + val allNetworkIds = matchedBlockchains.map { blockchain -> blockchain.toNetworkId() }.toSet() + val isSelectingAll = allNetworkIds.isEmpty() || !selectedNetworks.value.containsAll(allNetworkIds) + + analyticsSender.sendSelectAllNetworksTapped( + action = if (isSelectingAll) { + AddressBookEvents.SelectAllNetworksTapped.Action.SelectAll + } else { + AddressBookEvents.SelectAllNetworksTapped.Action.ClearAll + }, + scope = modelScope, + ) + + selectedNetworks.value = if (isSelectingAll) allNetworkIds else emptySet() + } + private fun onDoneClick() { val selected = selectedNetworks.value if (selected.isEmpty()) return diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/SelectNetworksStateController.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/SelectNetworksStateController.kt index 5b7275aeb0..23f08c09d7 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/SelectNetworksStateController.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/SelectNetworksStateController.kt @@ -36,6 +36,7 @@ internal class SelectNetworksStateController @Inject constructor() { onCloseClick = {}, ), networks = persistentListOf(), + selectAllButton = SelectNetworksUM.SelectAllButtonUM.Empty, doneButton = TangemButtonUM( text = TextReference.Res(R.string.common_done), type = TangemButtonType.Primary, @@ -43,5 +44,6 @@ internal class SelectNetworksStateController @Inject constructor() { onClick = {}, ), onBackClick = {}, + onSelectAllClick = {}, ) } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformer.kt index 63837c244a..e621a309a6 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformer.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformer.kt @@ -11,6 +11,7 @@ internal class UpdateNetworksContentTransformer( private val matchedBlockchains: List, private val query: String, private val selectedNetworkIds: Set, + private val isSearchActive: Boolean, private val onToggle: (networkId: String) -> Unit, ) : Transformer { @@ -36,9 +37,18 @@ internal class UpdateNetworksContentTransformer( } .toImmutableList() + val isAllSelected = matchedBlockchains.isNotEmpty() && + selectedNetworkIds.size == matchedBlockchains.size + val selectAllButton = when { + isSearchActive -> SelectNetworksUM.SelectAllButtonUM.Empty + isAllSelected -> SelectNetworksUM.SelectAllButtonUM.ClearAll + else -> SelectNetworksUM.SelectAllButtonUM.SelectAll + } + // Search field is owned by UpdateSelectNetworksSearchBarTransformer and intentionally left untouched here. return prevState.copy( networks = networks, + selectAllButton = selectAllButton, doneButton = prevState.doneButton.copy(isEnabled = selectedNetworkIds.isNotEmpty()), ) } diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateSelectNetworksInitialStateTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateSelectNetworksInitialStateTransformer.kt index 2421d856d8..1eec6bc86e 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateSelectNetworksInitialStateTransformer.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateSelectNetworksInitialStateTransformer.kt @@ -8,6 +8,7 @@ internal class UpdateSelectNetworksInitialStateTransformer( private val onActiveChange: (Boolean) -> Unit, private val onBackClick: () -> Unit, private val onDoneClick: () -> Unit, + private val onSelectAllClick: () -> Unit, ) : Transformer { override fun transform(prevState: SelectNetworksUM): SelectNetworksUM { @@ -20,6 +21,7 @@ internal class UpdateSelectNetworksInitialStateTransformer( ), doneButton = prevState.doneButton.copy(onClick = onDoneClick), onBackClick = onBackClick, + onSelectAllClick = onSelectAllClick, ) } } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/SelectNetworksContent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/SelectNetworksContent.kt index efa891b007..fbbb0f0675 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/SelectNetworksContent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/SelectNetworksContent.kt @@ -1,8 +1,15 @@ package com.tangem.features.addressbook.selectnetworks.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -10,6 +17,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,10 +33,9 @@ import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.checkbox.TangemCheckmark import com.tangem.core.ui.ds2.search.TangemSearch -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.addressbook.selectnetworks.ui.state.SelectNetworksUM @@ -79,11 +86,9 @@ internal fun SelectNetworksContent(state: SelectNetworksUM, modifier: Modifier = contentPadding = PaddingValues(horizontal = 16.dp), ) { item { - Text( - modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), - text = stringResourceSafe(R.string.common_available_networks), - style = TangemTheme.typography3.caption.medium, - color = TangemTheme.colors3.text.secondary, + NetworksHeader( + selectAllButton = state.selectAllButton, + onSelectAllClick = state.onSelectAllClick, ) } items(items = state.networks, key = NetworkItemUM::id) { item -> @@ -105,12 +110,64 @@ internal fun SelectNetworksContent(state: SelectNetworksUM, modifier: Modifier = } } +@Composable +private fun NetworksHeader( + selectAllButton: SelectNetworksUM.SelectAllButtonUM, + onSelectAllClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticManager = LocalHapticManager.current + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .weight(1f) + .padding(top = 16.dp, bottom = 4.dp), + text = stringResourceSafe(R.string.common_available_networks), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.secondary, + ) + AnimatedContent( + targetState = selectAllButton, + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 400)) togetherWith + fadeOut(animationSpec = tween(durationMillis = 400)) + }, + label = "SelectAllButton", + ) { button -> + if (!button.text.isNullOrEmpty()) { + Text( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + onSelectAllClick() + }, + ) + .padding(start = 4.dp, top = 16.dp, bottom = 4.dp), + text = button.text.resolveReference(), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.brand, + ) + } + } + } +} + @Composable private fun NetworkRow(item: NetworkItemUM) { Row( modifier = Modifier .fillMaxWidth() - .clickableSingle(onClick = item.onCheckedChange) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = item.onCheckedChange, + ) .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -182,6 +239,7 @@ private fun Preview_SelectNetworksContent() { onCheckedChange = {}, ), ), + selectAllButton = SelectNetworksUM.SelectAllButtonUM.SelectAll, doneButton = TangemButtonUM( text = TextReference.Res(R.string.common_done), type = TangemButtonType.Primary, @@ -189,6 +247,7 @@ private fun Preview_SelectNetworksContent() { onClick = {}, ), onBackClick = {}, + onSelectAllClick = {}, ), ) } diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/state/SelectNetworksUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/state/SelectNetworksUM.kt index eb565d9c15..5b73615fa6 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/state/SelectNetworksUM.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/selectnetworks/ui/state/SelectNetworksUM.kt @@ -2,16 +2,21 @@ package com.tangem.features.addressbook.selectnetworks.ui.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.core.ui.R import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds2.search.TangemSearch +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import kotlinx.collections.immutable.ImmutableList @Immutable internal data class SelectNetworksUM( val searchBar: TangemSearch.State, val networks: ImmutableList, + val selectAllButton: SelectAllButtonUM, val doneButton: TangemButtonUM, val onBackClick: () -> Unit, + val onSelectAllClick: () -> Unit, ) { @Immutable @@ -23,4 +28,10 @@ internal data class SelectNetworksUM( val isSelected: Boolean, val onCheckedChange: () -> Unit, ) + + enum class SelectAllButtonUM(val text: TextReference) { + SelectAll(text = resourceReference(R.string.address_book_select_all_networks)), + ClearAll(text = resourceReference(R.string.address_book_clear_all_networks)), + Empty(text = TextReference.EMPTY), + } } \ No newline at end of file diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModelTest.kt index 11ace5d9b1..ab4695ed82 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/model/SelectNetworksModelTest.kt @@ -5,15 +5,19 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.decompose.model.MutableParamsContainer import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.addressbook.common.AddressBookAnalyticsSender import com.tangem.features.addressbook.selectnetworks.DefaultSelectNetworksComponent import com.tangem.features.addressbook.selectnetworks.state.SelectNetworksStateController import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance @OptIn(ExperimentalCoroutinesApi::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -24,6 +28,8 @@ internal class SelectNetworksModelTest { private var model: SelectNetworksModel? = null + private val analyticsSender: AddressBookAnalyticsSender = mockk(relaxed = true) + @AfterEach fun tearDown() { model?.onDestroy() @@ -147,6 +153,7 @@ internal class SelectNetworksModelTest { paramsContainer = paramsContainer, dispatchers = testScope.createTestingCoroutineDispatcherProvider(), stateController = SelectNetworksStateController(), + analyticsSender = analyticsSender, ).also { model = it } } diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformerTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformerTest.kt new file mode 100644 index 0000000000..fd721eedbc --- /dev/null +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/selectnetworks/state/transformers/UpdateNetworksContentTransformerTest.kt @@ -0,0 +1,251 @@ +package com.tangem.features.addressbook.selectnetworks.state.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds2.search.TangemSearch +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.addressbook.selectnetworks.ui.state.SelectNetworksUM +import com.tangem.features.addressbook.selectnetworks.ui.state.SelectNetworksUM.SelectAllButtonUM +import com.tangem.test.core.ProvideTestModels +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class UpdateNetworksContentTransformerTest { + + private val ethereum = Blockchain.Ethereum + private val bitcoin = Blockchain.Bitcoin + private val polygon = Blockchain.Polygon + private val allBlockchains = listOf(ethereum, bitcoin, polygon) + + @Test + fun `GIVEN blank query WHEN transform THEN all matched networks shown in order`() { + // Arrange + val transformer = createTransformer(matchedBlockchains = allBlockchains, query = "") + + // Act + val result = transformer.transform(createPrevState()) + + // Assert + assertThat(result.networks.map { it.id }) + .containsExactly(ethereum.toNetworkId(), bitcoin.toNetworkId(), polygon.toNetworkId()) + .inOrder() + } + + @Test + fun `GIVEN network item WHEN transform THEN mapped fields match blockchain`() { + // Arrange + val transformer = createTransformer(matchedBlockchains = listOf(ethereum), query = "") + + // Act + val item = transformer.transform(createPrevState()).networks.single() + + // Assert + assertThat(item.id).isEqualTo(ethereum.toNetworkId()) + assertThat(item.name).isEqualTo(ethereum.fullName) + assertThat(item.symbol).isEqualTo(ethereum.currency) + } + + @Test + fun `GIVEN non-blank query WHEN transform THEN only matching networks shown`() { + // Arrange + val transformer = createTransformer(matchedBlockchains = allBlockchains, query = "bitcoin") + + // Act + val result = transformer.transform(createPrevState()) + + // Assert + assertThat(result.networks.map { it.id }).containsExactly(bitcoin.toNetworkId()) + } + + @Test + fun `GIVEN query matching nothing WHEN transform THEN no networks shown`() { + // Arrange + val transformer = createTransformer(matchedBlockchains = allBlockchains, query = "no-such-network") + + // Act + val result = transformer.transform(createPrevState()) + + // Assert + assertThat(result.networks).isEmpty() + } + + @Test + fun `GIVEN selected network ids WHEN transform THEN isSelected reflects selection`() { + // Arrange + val transformer = createTransformer( + matchedBlockchains = allBlockchains, + selectedNetworkIds = setOf(ethereum.toNetworkId(), polygon.toNetworkId()), + ) + + // Act + val selectionById = transformer.transform(createPrevState()).networks.associate { it.id to it.isSelected } + + // Assert + assertThat(selectionById).containsExactly( + ethereum.toNetworkId(), true, + bitcoin.toNetworkId(), false, + polygon.toNetworkId(), true, + ) + } + + @Test + fun `GIVEN item WHEN onCheckedChange invoked THEN onToggle called with network id`() { + // Arrange + val toggled = mutableListOf() + val transformer = createTransformer( + matchedBlockchains = listOf(ethereum), + onToggle = toggled::add, + ) + val item = transformer.transform(createPrevState()).networks.single() + + // Act + item.onCheckedChange() + + // Assert + assertThat(toggled).containsExactly(ethereum.toNetworkId()) + } + + @Test + fun `GIVEN non-empty selection WHEN transform THEN done button enabled`() { + // Arrange + val transformer = createTransformer( + matchedBlockchains = allBlockchains, + selectedNetworkIds = setOf(ethereum.toNetworkId()), + ) + + // Act + val result = transformer.transform(createPrevState(isDoneEnabled = false)) + + // Assert + assertThat(result.doneButton.isEnabled).isTrue() + } + + @Test + fun `GIVEN empty selection WHEN transform THEN done button disabled`() { + // Arrange + val transformer = createTransformer( + matchedBlockchains = allBlockchains, + selectedNetworkIds = emptySet(), + ) + + // Act + val result = transformer.transform(createPrevState(isDoneEnabled = true)) + + // Assert + assertThat(result.doneButton.isEnabled).isFalse() + } + + @ParameterizedTest + @ProvideTestModels + fun `WHEN transform THEN select-all button state matches selection and search`(model: SelectAllButtonModel) { + // Arrange + val transformer = createTransformer( + matchedBlockchains = model.matchedBlockchains, + selectedNetworkIds = model.selectedNetworkIds, + isSearchActive = model.isSearchActive, + ) + + // Act + val result = transformer.transform(createPrevState()) + + // Assert + assertThat(result.selectAllButton).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + SelectAllButtonModel( + description = "search active hides the button even when all selected", + matchedBlockchains = allBlockchains, + selectedNetworkIds = allBlockchains.map { it.toNetworkId() }.toSet(), + isSearchActive = true, + expected = SelectAllButtonUM.Empty, + ), + SelectAllButtonModel( + description = "search active hides the button when none selected", + matchedBlockchains = allBlockchains, + selectedNetworkIds = emptySet(), + isSearchActive = true, + expected = SelectAllButtonUM.Empty, + ), + SelectAllButtonModel( + description = "all selected shows Clear All", + matchedBlockchains = allBlockchains, + selectedNetworkIds = allBlockchains.map { it.toNetworkId() }.toSet(), + isSearchActive = false, + expected = SelectAllButtonUM.ClearAll, + ), + SelectAllButtonModel( + description = "partial selection shows Select All", + matchedBlockchains = allBlockchains, + selectedNetworkIds = setOf(ethereum.toNetworkId()), + isSearchActive = false, + expected = SelectAllButtonUM.SelectAll, + ), + SelectAllButtonModel( + description = "no selection shows Select All", + matchedBlockchains = allBlockchains, + selectedNetworkIds = emptySet(), + isSearchActive = false, + expected = SelectAllButtonUM.SelectAll, + ), + SelectAllButtonModel( + description = "no matched networks shows Select All", + matchedBlockchains = emptyList(), + selectedNetworkIds = emptySet(), + isSearchActive = false, + expected = SelectAllButtonUM.SelectAll, + ), + ) + + private fun createTransformer( + matchedBlockchains: List, + query: String = "", + selectedNetworkIds: Set = emptySet(), + isSearchActive: Boolean = false, + onToggle: (String) -> Unit = {}, + ) = UpdateNetworksContentTransformer( + matchedBlockchains = matchedBlockchains, + query = query, + selectedNetworkIds = selectedNetworkIds, + isSearchActive = isSearchActive, + onToggle = onToggle, + ) + + private fun createPrevState(isDoneEnabled: Boolean = false) = SelectNetworksUM( + searchBar = TangemSearch.State( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + networks = persistentListOf(), + selectAllButton = SelectAllButtonUM.Empty, + doneButton = TangemButtonUM( + text = TextReference.Res(R.string.common_done), + type = TangemButtonType.Primary, + isEnabled = isDoneEnabled, + onClick = {}, + ), + onBackClick = {}, + onSelectAllClick = {}, + ) + + internal data class SelectAllButtonModel( + val description: String, + val matchedBlockchains: List, + val selectedNetworkIds: Set, + val isSearchActive: Boolean, + val expected: SelectAllButtonUM, + ) { + override fun toString(): String = description + } +} \ No newline at end of file