From 8f09a5e87f1213540f84fc5fa182c33c32482c0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jun 2026 12:38:40 +0100 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + features/address-book/impl/build.gradle.kts | 6 + .../addressbook/component/AddressBookRoute.kt | 8 + .../component/DefaultAddressBookComponent.kt | 18 +- .../di/AddressBookComponentModule.kt | 6 + .../addressbook/di/AddressBookModelModule.kt | 6 + .../DefaultEditContactComponent.kt | 40 ++++ .../editcontact/EditContactComponent.kt | 15 ++ .../editcontact/contract/EditContactUM.kt | 26 +++ .../editcontact/model/EditContactModel.kt | 68 +++++++ .../editcontact/ui/EditContactContent.kt | 185 ++++++++++++++++++ .../list/DefaultAddressBookListComponent.kt | 6 +- .../list/contract/AddressBookListUM.kt | 5 +- .../list/model/AddressBookListModel.kt | 26 ++- .../list/ui/AddressBookEmptyScreen.kt | 27 ++- .../editcontact/model/EditContactModelTest.kt | 113 +++++++++++ 16 files changed, 539 insertions(+), 17 deletions(-) create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt create mode 100644 features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt create mode 100644 features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 57b95dc184..0c188411c0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -102,6 +102,7 @@ %d address %d addresses + Contact Contact name Copy address We couldn’t create contact. Please try again later. diff --git a/features/address-book/impl/build.gradle.kts b/features/address-book/impl/build.gradle.kts index 0e9ca8c98a..4711602801 100644 --- a/features/address-book/impl/build.gradle.kts +++ b/features/address-book/impl/build.gradle.kts @@ -19,6 +19,9 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.addressBook) + /** Common */ + implementation(projects.common.ui) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.decompose) @@ -41,4 +44,7 @@ dependencies { /** Other */ implementation(deps.kotlin.immutable.collections) + + /** Tests */ + testImplementation(projects.test.core) } \ No newline at end of file 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 index baa9ee7021..49e12ab00f 100644 --- 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 @@ -7,4 +7,12 @@ internal sealed class AddressBookRoute { @Serializable data object List : AddressBookRoute() + + /** + * if [contactId] is not null we should fetch existing contact + */ + @Serializable + data class EditContact( + val contactId: String? = null, + ) : 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 index 78c5312faa..4b4ee9140a 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 @@ -9,11 +9,15 @@ 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.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.pushNew import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.addressbook.model.ContactId import com.tangem.features.addressbook.AddressBookComponent import com.tangem.features.addressbook.list.AddressBookListComponent +import com.tangem.features.addressbook.editcontact.EditContactComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -22,6 +26,7 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: AddressBookComponent.Params, private val addressBookListComponentFactory: AddressBookListComponent.Factory, + private val editContactComponentFactory: EditContactComponent.Factory, ) : AddressBookComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -51,11 +56,16 @@ internal class DefaultAddressBookComponent @AssistedInject constructor( context = childByContext(componentContext), params = AddressBookListComponent.Params( onContactClick = { contactId -> - // TODO [REDACTED_TASK_KEY] router.push(EditContact(contactId)) - }, - onAddContactClick = { - // TODO [REDACTED_TASK_KEY] router.push(AddContact) + navigation.pushNew(AddressBookRoute.EditContact(contactId)) }, + onAddContactClick = { navigation.pushNew(AddressBookRoute.EditContact()) }, + ), + ) + is AddressBookRoute.EditContact -> editContactComponentFactory.create( + context = childByContext(componentContext), + params = EditContactComponent.Params( + contactId = config.contactId?.let(::ContactId), + onBackClick = { navigation.pop() }, ), ) } 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 index 00e407405f..188884bab3 100644 --- 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 @@ -4,6 +4,8 @@ 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 com.tangem.features.addressbook.editcontact.DefaultEditContactComponent +import com.tangem.features.addressbook.editcontact.EditContactComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +25,8 @@ internal interface AddressBookComponentModule { fun bindAddressBookListComponentFactory( factory: DefaultAddressBookListComponent.Factory, ): AddressBookListComponent.Factory + + @Binds + @Singleton + fun bindEditContactComponentFactory(factory: DefaultEditContactComponent.Factory): EditContactComponent.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 index 7c666b0b6d..0fb085f06d 100644 --- 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 @@ -3,6 +3,7 @@ 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 com.tangem.features.addressbook.editcontact.model.EditContactModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -17,4 +18,9 @@ internal interface AddressBookModelModule { @IntoMap @ClassKey(AddressBookListModel::class) fun bindAddressBookModel(model: AddressBookListModel): Model + + @Binds + @IntoMap + @ClassKey(EditContactModel::class) + fun bindEditContactModel(model: EditContactModel): Model } \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt new file mode 100644 index 0000000000..8f83105d52 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.addressbook.editcontact + +import androidx.activity.compose.BackHandler +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.editcontact.model.EditContactModel +import com.tangem.features.addressbook.editcontact.ui.EditContactContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultEditContactComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: EditContactComponent.Params, +) : EditContactComponent, AppComponentContext by context { + + private val model: EditContactModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + EditContactContent( + state = state, + modifier = modifier, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : EditContactComponent.Factory { + override fun create( + context: AppComponentContext, + params: EditContactComponent.Params, + ): DefaultEditContactComponent + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt new file mode 100644 index 0000000000..ede28263b7 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.addressbook.editcontact + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.addressbook.model.ContactId + +internal interface EditContactComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + data class Params( + val contactId: ContactId?, + val onBackClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt new file mode 100644 index 0000000000..2a54242a81 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.addressbook.editcontact.contract + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class EditContactUM( + val title: TextReference, + val name: String, + val namePlaceholder: TextReference, + val portfolioIcon: AccountIconUM.CryptoPortfolio, + val colors: Colors, + val onNameChange: (String) -> Unit, + val onCloseClick: () -> Unit, +) { + + @Immutable + data class Colors( + val selected: CryptoPortfolioIcon.Color, + val list: ImmutableList, + val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit, + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1f1035618b --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt @@ -0,0 +1,68 @@ +package com.tangem.features.addressbook.editcontact.model + +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.addressbook.editcontact.EditContactComponent +import com.tangem.features.addressbook.editcontact.contract.EditContactUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class EditContactModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: EditContactComponent.Params = paramsContainer.require() + + val state: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun onNameChange(name: String) { + state.update { it.copy(name = name) } + } + + private fun onColorSelect(color: CryptoPortfolioIcon.Color) { + state.update { oldState -> + oldState.copy( + colors = oldState.colors.copy(selected = color), + portfolioIcon = oldState.portfolioIcon.copy(color = color), + ) + } + } + + private fun getInitialState(): EditContactUM { + val colors = CryptoPortfolioIcon.Color.entries.toImmutableList() + val selectedColor = colors.first() + val titleResId = if (params.contactId == null) { + R.string.address_book_new_contact + } else { + R.string.address_book_contact + } + return EditContactUM( + title = resourceReference(titleResId), + name = "", + namePlaceholder = resourceReference(R.string.address_book_new_contact), + portfolioIcon = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Letter, + color = selectedColor, + ), + colors = EditContactUM.Colors( + selected = selectedColor, + list = colors, + onColorSelect = ::onColorSelect, + ), + onNameChange = ::onNameChange, + onCloseClick = params.onBackClick, + ) + } +} \ No newline at end of file diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt new file mode 100644 index 0000000000..2183d33c43 --- /dev/null +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt @@ -0,0 +1,185 @@ +package com.tangem.features.addressbook.editcontact.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.R +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.fields.AutoSizeTextField +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.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.addressbook.editcontact.contract.EditContactUM +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background(color = TangemTheme.colors3.bg.primary) + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TangemTopBar( + modifier = Modifier.statusBarsPadding(), + title = state.title, + endContent = { + TangemButton( + iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24), + onClick = state.onCloseClick, + size = TangemButton.Size.X11, + variant = TangemButton.Variant.Material, + ) + }, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ContactSummary(state = state) + ContactColor(colors = state.colors) + } + } +} + +@Composable +private fun ContactSummary(state: EditContactUM) { + val avatarName = state.name.ifBlank { state.namePlaceholder.resolveReference() } + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors3.bg.secondary), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + AccountIcon( + name = stringReference(avatarName), + icon = state.portfolioIcon, + size = AccountIconSize.Large, + ) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResourceSafe(R.string.address_book_contact_name), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.tertiary, + ) + Spacer(modifier = Modifier.height(2.dp)) + + AutoSizeTextField( + value = state.name, + onValueChange = state.onNameChange, + centered = true, + singleLine = true, + placeholder = state.namePlaceholder, + textStyle = TangemTheme.typography3.heading.medium, + color = TangemTheme.colors3.text.primary, + placeholderColor = TangemTheme.colors3.text.tertiary, + ) + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Suppress("MagicNumber") +@Composable +private fun ContactColor(colors: EditContactUM.Colors) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors3.bg.secondary), + ) { + FlowRow( + maxItemsInEachRow = 6, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + colors.list.fastForEach { color -> + val isSelected = color == colors.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clip(CircleShape) + .clickable(onClick = { colors.onColorSelect(color) }) + .size(48.dp), + ) { + if (isSelected) { + Box( + modifier = Modifier + .size(47.dp) + .border(2.dp, color.getUiColor(), shape = CircleShape), + ) + Box( + modifier = Modifier + .size(36.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_EditContactContent() { + val colors = CryptoPortfolioIcon.Color.entries.toImmutableList() + TangemThemePreview { + EditContactContent( + state = EditContactUM( + title = stringReference("New contact"), + name = "", + namePlaceholder = stringReference("New contact"), + portfolioIcon = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Letter, + color = colors.first(), + ), + colors = EditContactUM.Colors( + selected = colors.first(), + list = colors, + onColorSelect = {}, + ), + onNameChange = {}, + onCloseClick = {}, + ), + ) + } +} \ 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 416edbd08c..63733b90f9 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 @@ -23,9 +23,9 @@ internal class DefaultAddressBookListComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - when (state) { - AddressBookListUM.Empty -> AddressBookEmptyScreen( - onAddContactClick = params.onAddContactClick, + when (val addressBookListUM = state) { + is AddressBookListUM.Empty -> AddressBookEmptyScreen( + tangemButtonUM = addressBookListUM.tangemButtonUM, onBackClick = router::pop, modifier = modifier, ) 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 f5cf907646..4c0c74bab4 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 @@ -1,12 +1,15 @@ package com.tangem.features.addressbook.list.contract import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.domain.addressbook.model.Contact import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class AddressBookListUM { - data object Empty : AddressBookListUM() + data class Empty( + val tangemButtonUM: TangemButtonUM, + ) : 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/model/AddressBookListModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt index 224fa6edd0..27039d82aa 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,6 +2,16 @@ 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.core.ui.R +import com.tangem.core.ui.R.drawable.ic_plus_24 +import com.tangem.core.ui.ds.button.TangemButtonIconPosition +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.addressbook.list.AddressBookListComponent import com.tangem.features.addressbook.list.contract.AddressBookListUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -10,10 +20,24 @@ import javax.inject.Inject @ModelScoped internal class AddressBookListModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { + private val params = paramsContainer.require() + val state: StateFlow = MutableStateFlow( - AddressBookListUM.Empty, + AddressBookListUM.Empty( + tangemButtonUM = TangemButtonUM( + text = TextReference.Res(R.string.address_book_new_contact), + tangemIconUM = TangemIconUM.Icon( + iconRes = ic_plus_24, + tintReference = { TangemTheme.colors3.text.inverse.primary }, + ), + iconPosition = TangemButtonIconPosition.End, + type = TangemButtonType.Primary, + onClick = params.onAddContactClick, + ), + ), ) } \ 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 8c258eb5eb..b1045b5cc2 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 @@ -14,17 +14,21 @@ 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.ds.button.PrimaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonIconPosition +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM 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.TextReference 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, + tangemButtonUM: TangemButtonUM, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -40,19 +44,17 @@ internal fun AddressBookEmptyScreen( iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24), onClick = onBackClick, size = TangemButton.Size.X11, - variant = TangemButton.Variant.Secondary, + variant = TangemButton.Variant.Material, ) }, ) NoContactInfo() - PrimaryButtonIconEnd( + PrimaryTangemButton( 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, + buttonUM = tangemButtonUM, ) } } @@ -104,5 +106,14 @@ 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 = {}, onBackClick = {}) + AddressBookEmptyScreen( + tangemButtonUM = TangemButtonUM( + text = TextReference.Res(R.string.address_book_new_contact), + tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_plus_24), + iconPosition = TangemButtonIconPosition.End, + type = TangemButtonType.Secondary, + onClick = {}, + ), + onBackClick = {}, + ) } \ 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 new file mode 100644 index 0000000000..d44b437435 --- /dev/null +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt @@ -0,0 +1,113 @@ +package com.tangem.features.addressbook.editcontact.model + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.decompose.model.MutableParamsContainer +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.addressbook.model.ContactId +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.addressbook.editcontact.EditContactComponent +import com.tangem.features.addressbook.editcontact.contract.EditContactUM +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class EditContactModelTest { + + @Test + fun `WHEN model created THEN initial state is correct`() = runTest { + val expectedColors = CryptoPortfolioIcon.Color.entries.toImmutableList() + val expectedSelectedColor = expectedColors.first() + + val model = createModel(testScope = this) + val state = model.state.value + + val expected = EditContactUM( + title = resourceReference(R.string.address_book_new_contact), + name = "", + namePlaceholder = resourceReference(R.string.address_book_new_contact), + portfolioIcon = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Letter, + color = expectedSelectedColor, + ), + colors = EditContactUM.Colors( + selected = expectedSelectedColor, + list = expectedColors, + onColorSelect = state.colors.onColorSelect, + ), + onNameChange = state.onNameChange, + onCloseClick = state.onCloseClick, + ) + assertThat(state).isEqualTo(expected) + } + + @Test + fun `GIVEN existing contactId WHEN model created THEN title is contact`() = runTest { + // Arrange + val params = EditContactComponent.Params( + contactId = ContactId(value = "contact-id"), + onBackClick = {}, + ) + + // Act + val model = createModel(testScope = this, params = params) + val state = model.state.value + + // Assert + assertThat(state.title).isEqualTo(resourceReference(R.string.address_book_contact)) + } + + @Test + fun `GIVEN initial state WHEN onNameChange THEN name updated`() = runTest { + val model = createModel(testScope = this) + val newName = "Satoshi" + + model.state.value.onNameChange(newName) + + assertThat(model.state.value.name).isEqualTo(newName) + } + + @Test + fun `GIVEN initial state WHEN onColorSelect THEN selected color and portfolio icon updated`() = runTest { + val model = createModel(testScope = this) + val newColor = CryptoPortfolioIcon.Color.entries.last() + + model.state.value.colors.onColorSelect(newColor) + + val state = model.state.value + assertThat(state.colors.selected).isEqualTo(newColor) + assertThat(state.portfolioIcon.color).isEqualTo(newColor) + } + + private fun createModel( + testScope: TestScope, + params: EditContactComponent.Params = EditContactComponent.Params( + contactId = null, + onBackClick = {}, + ), + paramsContainer: ParamsContainer = MutableParamsContainer(value = params), + ): EditContactModel { + return EditContactModel( + paramsContainer = paramsContainer, + dispatchers = testScope.createTestingCoroutineDispatcherProvider(), + ) + } + + private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider { + val testDispatcher = StandardTestDispatcher(testScheduler) + return TestingCoroutineDispatcherProvider( + main = testDispatcher, + mainImmediate = testDispatcher, + io = testDispatcher, + default = testDispatcher, + single = testDispatcher, + ) + } +} \ No newline at end of file