From 4807321b9a400f3762cd440cee04f587421325a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jun 2026 13:55:32 +0100 Subject: [PATCH 1/5] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 9 ++ .../com/tangem/common/routing/AppRoute.kt | 5 ++ .../addressbook/AddressBookComponent.kt | 11 +++ features/address-book/impl/build.gradle.kts | 12 +++ .../addressbook/component/AddressBookRoute.kt | 10 +++ .../component/DefaultAddressBookComponent.kt | 65 ++++++++++++++ .../di/AddressBookComponentModule.kt | 26 ++++++ .../addressbook/di/AddressBookModelModule.kt | 20 +++++ .../list/AddressBookListComponent.kt | 11 +++ .../list/DefaultAddressBookListComponent.kt | 42 +++++++++ .../list/contract/AddressBookListContract.kt | 16 ++++ .../list/model/AddressBookListModel.kt | 34 +++++++ .../list/ui/AddressBookEmptyScreen.kt | 88 +++++++++++++++++++ .../features/details/utils/ItemsBuilder.kt | 2 +- .../details/utils/ItemsBuilderTest.kt | 47 ++++++++++ 15 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 features/address-book/api/src/main/kotlin/com/tangem/features/addressbook/AddressBookComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 1684f52754..816ba6c2eb 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,6 +11,7 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.addressbook.AddressBookComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.createwalletstart.CreateWalletStartComponent import com.tangem.features.details.component.DetailsComponent @@ -115,6 +116,7 @@ internal class ChildFactory @Inject constructor( private val surveyComponentFactory: SurveyComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory, + private val addressBookComponentFactory: AddressBookComponent.Factory, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -744,6 +746,13 @@ internal class ChildFactory @Inject constructor( componentFactory = feedEntryComponentFactory, ) } + is AppRoute.AddressBook -> { + createComponentChild( + context = context, + params = AddressBookComponent.Params(route.predefinedAddress), + componentFactory = addressBookComponentFactory, + ) + } } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 5cda954ac5..73d0e6bebb 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -172,6 +172,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions") + @Serializable + data class AddressBook( + val predefinedAddress: String? = null, + ) : AppRoute(path = "/address_book/predefinedAddress/$predefinedAddress") + @Serializable data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") { diff --git a/features/address-book/api/src/main/kotlin/com/tangem/features/addressbook/AddressBookComponent.kt b/features/address-book/api/src/main/kotlin/com/tangem/features/addressbook/AddressBookComponent.kt new file mode 100644 index 0000000000..e9fe9ddb22 --- /dev/null +++ b/features/address-book/api/src/main/kotlin/com/tangem/features/addressbook/AddressBookComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.addressbook + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface AddressBookComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + data class Params(val predefinedAddress: String?) +} \ No newline at end of file diff --git a/features/address-book/impl/build.gradle.kts b/features/address-book/impl/build.gradle.kts index f91cde71b1..c36b2239ed 100644 --- a/features/address-book/impl/build.gradle.kts +++ b/features/address-book/impl/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) id("configuration") @@ -14,13 +15,24 @@ dependencies { /** Api */ implementation(projects.features.addressBook.api) + /** Domain */ + implementation(projects.domain.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.utils) /** Compose */ implementation(deps.compose.runtime) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + implementation(deps.decompose.ext.compose) /** DI */ implementation(deps.hilt.android) diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt new file mode 100644 index 0000000000..baa9ee7021 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt @@ -0,0 +1,10 @@ +package com.tangem.features.addressbook.component + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class AddressBookRoute { + + @Serializable + data object List : AddressBookRoute() +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt new file mode 100644 index 0000000000..8a816cec10 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt @@ -0,0 +1,65 @@ +package com.tangem.features.addressbook.component + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.addressbook.AddressBookComponent +import com.tangem.features.addressbook.list.AddressBookListComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAddressBookComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: AddressBookComponent.Params, + private val addressBookListComponentFactory: AddressBookListComponent.Factory, +) : AddressBookComponent, AppComponentContext by context { + + private val navigation = StackNavigation() + + private val contentStack = childStack( + key = "address_book_stack", + source = navigation, + serializer = AddressBookRoute.serializer(), + initialConfiguration = AddressBookRoute.List, + handleBackButton = false, + childFactory = ::screenChild, + ) + + @Suppress("ReusedModifierInstance") + @Composable + override fun Content(modifier: Modifier) { + val childStack by contentStack.subscribeAsState() + + BackHandler(onBack = router::pop) + Children(stack = childStack, animation = stackAnimation()) { child -> + child.instance.Content(modifier = modifier) + } + } + + private fun screenChild(config: AddressBookRoute, componentContext: ComponentContext): ComposableContentComponent = + when (config) { + AddressBookRoute.List -> addressBookListComponentFactory.create( + context = childByContext(componentContext), + params = AddressBookListComponent.Params, + ) + } + + @AssistedFactory + interface Factory : AddressBookComponent.Factory { + override fun create( + context: AppComponentContext, + params: AddressBookComponent.Params, + ): DefaultAddressBookComponent + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt new file mode 100644 index 0000000000..00e407405f --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.addressbook.di + +import com.tangem.features.addressbook.AddressBookComponent +import com.tangem.features.addressbook.component.DefaultAddressBookComponent +import com.tangem.features.addressbook.list.AddressBookListComponent +import com.tangem.features.addressbook.list.DefaultAddressBookListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AddressBookComponentModule { + + @Binds + @Singleton + fun bindAddressBookComponentFactory(factory: DefaultAddressBookComponent.Factory): AddressBookComponent.Factory + + @Binds + @Singleton + fun bindAddressBookListComponentFactory( + factory: DefaultAddressBookListComponent.Factory, + ): AddressBookListComponent.Factory +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt new file mode 100644 index 0000000000..7c666b0b6d --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.addressbook.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.addressbook.list.model.AddressBookListModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface AddressBookModelModule { + + @Binds + @IntoMap + @ClassKey(AddressBookListModel::class) + fun bindAddressBookModel(model: AddressBookListModel): Model +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt new file mode 100644 index 0000000000..280b889082 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.addressbook.list + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +internal interface AddressBookListComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + data object Params +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a5ce1d1ea3 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.addressbook.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.addressbook.list.contract.AddressBookListEvent +import com.tangem.features.addressbook.list.contract.AddressBookListUM +import com.tangem.features.addressbook.list.model.AddressBookListModel +import com.tangem.features.addressbook.list.ui.AddressBookEmptyScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAddressBookListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: AddressBookListComponent.Params, +) : AddressBookListComponent, AppComponentContext by context { + + private val model: AddressBookListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + when (state) { + AddressBookListUM.Empty -> AddressBookEmptyScreen( + onAddContactClick = { model.onAction(event = AddressBookListEvent.NewContactClick) }, + modifier = modifier, + ) + } + } + + @AssistedFactory + interface Factory : AddressBookListComponent.Factory { + override fun create( + context: AppComponentContext, + params: AddressBookListComponent.Params, + ): DefaultAddressBookListComponent + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt new file mode 100644 index 0000000000..95f4736361 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt @@ -0,0 +1,16 @@ +package com.tangem.features.addressbook.list.contract + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface AddressBookListUM { + + data object Empty : AddressBookListUM +} + +internal sealed interface AddressBookListEvent { + data object NewContactClick : AddressBookListEvent + data class ContactClick(val contactId: String) : AddressBookListEvent + data class ChipClick(val walletId: String) : AddressBookListEvent + data class SearchInput(val query: String) : AddressBookListEvent +} \ No newline at end of file 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 new file mode 100644 index 0000000000..966a3b9205 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt @@ -0,0 +1,34 @@ +package com.tangem.features.addressbook.list.model + +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.list.AddressBookListComponent +import com.tangem.features.addressbook.list.contract.AddressBookListEvent +import com.tangem.features.addressbook.list.contract.AddressBookListUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class AddressBookListModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: AddressBookListComponent.Params = paramsContainer.require() + + val state: StateFlow = MutableStateFlow( + AddressBookListUM.Empty, + ) + + fun onAction(event: AddressBookListEvent) { + when (event) { + AddressBookListEvent.NewContactClick -> params + is AddressBookListEvent.ContactClick -> Unit + is AddressBookListEvent.ChipClick -> Unit + is AddressBookListEvent.SearchInput -> Unit + } + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt new file mode 100644 index 0000000000..5c39b70eee --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt @@ -0,0 +1,88 @@ +package com.tangem.features.addressbook.list.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun AddressBookEmptyScreen(onAddContactClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NoContactInfo() + PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp), + text = stringResourceSafe(R.string.address_book_add_contact), + iconResId = R.drawable.ic_plus_24, + onClick = onAddContactClick, + ) + } +} + +@Composable +private fun ColumnScope.NoContactInfo() { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ContactImage() + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = stringResourceSafe(R.string.address_book_no_contacts), + color = TangemTheme.colors3.text.primary, + style = TangemTheme.typography3.heading.medium, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = stringResourceSafe(R.string.address_book_no_contacts_description), + color = TangemTheme.colors3.text.secondary, + style = TangemTheme.typography3.body.medium, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun ContactImage() { + Box( + modifier = Modifier + .size(80.dp) + .background( + color = TangemTheme.colors3.bg.status.infoSubtle, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(R.drawable.ic_contact_20), + contentDescription = stringResourceSafe(R.string.address_book_no_contacts), + modifier = Modifier.size(28.dp), + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_AddressBookEmptyScreen() { + AddressBookEmptyScreen(onAddContactClick = {}) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index c309485bf6..df2e653821 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -114,7 +114,7 @@ internal class ItemsBuilder @Inject constructor( private fun buildAddressBookButton(): DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook { return DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook( - onClick = { }, + onClick = { router.push(AppRoute.AddressBook()) }, ) } diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt index 5aa6f04da9..d6962201eb 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt @@ -1,6 +1,7 @@ package com.tangem.features.details.utils import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference @@ -11,6 +12,7 @@ import com.tangem.features.details.impl.R import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -90,6 +92,51 @@ internal class ItemsBuilderTest { ) } + @Test + fun `GIVEN standalone walletConnect block WHEN item clicked THEN router pushes WalletConnectSessions`() { + // Arrange + val result = buildAll(isWalletConnectAvailable = true, isAddressBookAvailable = false) + val walletConnect = result.first() as DetailsItemUM.WalletConnect + + // Act + walletConnect.onClick() + + // Assert + verify(exactly = 1) { router.push(route = AppRoute.WalletConnectSessions(USER_WALLET_ID), onComplete = any()) } + } + + @Test + fun `GIVEN combined block walletConnect item WHEN clicked THEN router pushes WalletConnectSessions`() { + // Arrange + val result = buildAll(isWalletConnectAvailable = true, isAddressBookAvailable = true) + val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock + val walletConnect = block.items + .filterIsInstance() + .single() + + // Act + walletConnect.onClick() + + // Assert + verify(exactly = 1) { router.push(route = AppRoute.WalletConnectSessions(USER_WALLET_ID), onComplete = any()) } + } + + @Test + fun `GIVEN combined block addressBook item WHEN clicked THEN router pushes AddressBook`() { + // Arrange + val result = buildAll(isWalletConnectAvailable = true, isAddressBookAvailable = true) + val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock + val addressBook = block.items + .filterIsInstance() + .single() + + // Act + addressBook.onClick() + + // Assert + verify(exactly = 1) { router.push(route = AppRoute.AddressBook(), onComplete = any()) } + } + @Test fun `GIVEN walletConnect AND addressBook unavailable WHEN buildAll THEN no walletConnect block`() { // Act From 1560706cba84bde670b9c2b51ac171936aaa9c6c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jun 2026 15:16:11 +0100 Subject: [PATCH 2/5] Updated on 2026-08-14 --- .../kotlin/com/tangem/features/details/ui/DetailsScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 5d99380d75..427c80adce 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -163,7 +163,7 @@ private fun Block( BlockCard { WalletConnectAddressBookBlockItems( items = model.items, - modifier = itemModifier.padding(12.dp), + modifier = itemModifier, ) } } @@ -184,14 +184,14 @@ private fun WalletConnectAddressBookBlockItems( items.fastForEach { item -> when (item) { is DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect -> InputRowImageBase( - modifier = modifier.clickable(onClick = item.onClick), + modifier = modifier.clickable(onClick = item.onClick).padding(12.dp), iconResVector = R.drawable.ic_wallet_connect_24, iconTint = TangemTheme.colors.icon.primary1, subtitle = TextReference.Res(R.string.wallet_connect_title), caption = TextReference.Res(R.string.wallet_connect_subtitle), ) is DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook -> InputRowImageBase( - modifier = modifier.clickable(onClick = item.onClick), + modifier = modifier.clickable(onClick = item.onClick).padding(12.dp), iconResVector = R.drawable.ic_contact_20, iconTint = TangemTheme.colors.icon.accent, subtitle = TextReference.Res(R.string.address_book_title), From eef969ae57476c51cecd8334a95a290334290222 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jun 2026 16:39:52 +0100 Subject: [PATCH 3/5] Updated on 2026-08-14 --- features/address-book/impl/build.gradle.kts | 1 + .../component/DefaultAddressBookComponent.kt | 9 ++++++++- .../list/AddressBookListComponent.kt | 5 ++++- .../list/DefaultAddressBookListComponent.kt | 7 ++++--- .../list/contract/AddressBookListContract.kt | 16 ---------------- .../list/contract/AddressBookListUM.kt | 11 +++++++++++ .../list/model/AddressBookListModel.kt | 15 --------------- .../list/ui/AddressBookEmptyScreen.kt | 18 ++++++++++++++++-- 8 files changed, 44 insertions(+), 38 deletions(-) delete mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt diff --git a/features/address-book/impl/build.gradle.kts b/features/address-book/impl/build.gradle.kts index c36b2239ed..8ecab403c4 100644 --- a/features/address-book/impl/build.gradle.kts +++ b/features/address-book/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /** Domain */ implementation(projects.domain.models) + implementation(projects.domain.addressBook) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt index 8a816cec10..96888bc360 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt @@ -51,7 +51,14 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( when (config) { AddressBookRoute.List -> addressBookListComponentFactory.create( context = childByContext(componentContext), - params = AddressBookListComponent.Params, + params = AddressBookListComponent.Params( + onContactClick = { contactId -> + // TODO [REDACTED_TASK_KEY] router.push(EditContact(contactId)) + }, + onAddContactClick = { + // TODO [REDACTED_TASK_KEY] router.push(AddContact) + }, + ), ) } diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt index 280b889082..0072a79d9f 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt @@ -7,5 +7,8 @@ internal interface AddressBookListComponent : ComposableContentComponent { interface Factory : ComponentFactory - data object Params + data class Params( + val onContactClick: (String) -> Unit, + val onAddContactClick: () -> Unit, + ) } \ No newline at end of file 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 a5ce1d1ea3..416edbd08c 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 @@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.addressbook.list.contract.AddressBookListEvent import com.tangem.features.addressbook.list.contract.AddressBookListUM import com.tangem.features.addressbook.list.model.AddressBookListModel import com.tangem.features.addressbook.list.ui.AddressBookEmptyScreen @@ -16,7 +15,7 @@ import dagger.assisted.AssistedInject internal class DefaultAddressBookListComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: AddressBookListComponent.Params, + @Assisted val params: AddressBookListComponent.Params, ) : AddressBookListComponent, AppComponentContext by context { private val model: AddressBookListModel = getOrCreateModel(params) @@ -26,9 +25,11 @@ internal class DefaultAddressBookListComponent @AssistedInject constructor( val state by model.state.collectAsStateWithLifecycle() when (state) { AddressBookListUM.Empty -> AddressBookEmptyScreen( - onAddContactClick = { model.onAction(event = AddressBookListEvent.NewContactClick) }, + onAddContactClick = params.onAddContactClick, + onBackClick = router::pop, modifier = modifier, ) + is AddressBookListUM.AddressList -> TODO("[REDACTED_TASK_KEY]") } } diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt deleted file mode 100644 index 95f4736361..0000000000 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListContract.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.addressbook.list.contract - -import androidx.compose.runtime.Immutable - -@Immutable -internal sealed interface AddressBookListUM { - - data object Empty : AddressBookListUM -} - -internal sealed interface AddressBookListEvent { - data object NewContactClick : AddressBookListEvent - data class ContactClick(val contactId: String) : AddressBookListEvent - data class ChipClick(val walletId: String) : AddressBookListEvent - data class SearchInput(val query: String) : AddressBookListEvent -} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt new file mode 100644 index 0000000000..3b50fcb3c1 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.addressbook.list.contract + +import androidx.compose.runtime.Immutable +import com.tangem.domain.addressbook.model.Contact + +@Immutable +internal sealed class AddressBookListUM { + + data object Empty : AddressBookListUM() + data class AddressList(val contacts: List) : AddressBookListUM() +} \ No newline at end of file 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 966a3b9205..224fa6edd0 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 @@ -2,9 +2,6 @@ package com.tangem.features.addressbook.list.model 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.list.AddressBookListComponent -import com.tangem.features.addressbook.list.contract.AddressBookListEvent import com.tangem.features.addressbook.list.contract.AddressBookListUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -13,22 +10,10 @@ import javax.inject.Inject @ModelScoped internal class AddressBookListModel @Inject constructor( - paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - private val params: AddressBookListComponent.Params = paramsContainer.require() - val state: StateFlow = MutableStateFlow( AddressBookListUM.Empty, ) - - fun onAction(event: AddressBookListEvent) { - when (event) { - AddressBookListEvent.NewContactClick -> params - is AddressBookListEvent.ContactClick -> Unit - is AddressBookListEvent.ChipClick -> Unit - is AddressBookListEvent.SearchInput -> Unit - } - } } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt index 5c39b70eee..0eb8eb4765 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt @@ -15,15 +15,29 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @Composable -internal fun AddressBookEmptyScreen(onAddContactClick: () -> Unit, modifier: Modifier = Modifier) { +internal fun AddressBookEmptyScreen( + onAddContactClick: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = resourceReference(R.string.address_book_title), + startButton = TopAppBarButtonUM.Back( + onBackClicked = onBackClick, + ), + ) NoContactInfo() PrimaryButtonIconEnd( modifier = Modifier @@ -84,5 +98,5 @@ private fun ContactImage() { @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun Preview_AddressBookEmptyScreen() { - AddressBookEmptyScreen(onAddContactClick = {}) + AddressBookEmptyScreen(onAddContactClick = {}, onBackClick = {}) } \ No newline at end of file From b6dcdc4d8764335aa344bcf5fb0fd94786a01332 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jun 2026 17:04:07 +0100 Subject: [PATCH 4/5] Updated on 2026-08-14 --- .../addressbook/component/DefaultAddressBookComponent.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt index 96888bc360..78c5312faa 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt @@ -1,6 +1,5 @@ package com.tangem.features.addressbook.component -import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -41,7 +40,6 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val childStack by contentStack.subscribeAsState() - BackHandler(onBack = router::pop) Children(stack = childStack, animation = stackAnimation()) { child -> child.instance.Content(modifier = modifier) } From d32f14390330edc9ab339f2cb6dbe2f846ab58ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jun 2026 10:12:31 +0100 Subject: [PATCH 5/5] Updated on 2026-08-14 --- features/address-book/impl/build.gradle.kts | 3 +++ .../list/contract/AddressBookListUM.kt | 3 ++- .../list/ui/AddressBookEmptyScreen.kt | 18 ++++++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/features/address-book/impl/build.gradle.kts b/features/address-book/impl/build.gradle.kts index 8ecab403c4..0e9ca8c98a 100644 --- a/features/address-book/impl/build.gradle.kts +++ b/features/address-book/impl/build.gradle.kts @@ -38,4 +38,7 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt index 3b50fcb3c1..f5cf907646 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt @@ -2,10 +2,11 @@ package com.tangem.features.addressbook.list.contract import androidx.compose.runtime.Immutable import com.tangem.domain.addressbook.model.Contact +import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class AddressBookListUM { data object Empty : AddressBookListUM() - data class AddressList(val contacts: List) : AddressBookListUM() + data class AddressList(val contacts: ImmutableList) : AddressBookListUM() } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt index 0eb8eb4765..8c258eb5eb 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt @@ -15,8 +15,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -31,12 +32,17 @@ internal fun AddressBookEmptyScreen( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { - TangemTopAppBar( + TangemTopBar( modifier = Modifier.statusBarsPadding(), title = resourceReference(R.string.address_book_title), - startButton = TopAppBarButtonUM.Back( - onBackClicked = onBackClick, - ), + startContent = { + TangemButton( + iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24), + onClick = onBackClick, + size = TangemButton.Size.X11, + variant = TangemButton.Variant.Secondary, + ) + }, ) NoContactInfo() PrimaryButtonIconEnd(