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 1e1942f867..8f1f852eb9 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
@@ -21,7 +21,6 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent
-import com.tangem.features.survey.SurveyComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
@@ -37,9 +36,10 @@ import com.tangem.features.send.api.NFTSendComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
+import com.tangem.features.survey.SurveyComponent
import com.tangem.features.swap.SwapComponent
-import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
+import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
import com.tangem.features.tokendetails.TokenDetailsComponent
@@ -758,7 +758,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.AddressBook -> {
createComponentChild(
context = context,
- params = AddressBookComponent.Params(route.predefinedAddress),
+ params = AddressBookComponent.Params(addressBookOpenMode = route.addressBookOpenMode),
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 12df52d117..1f04371c14 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
@@ -6,6 +6,7 @@ import android.annotation.SuppressLint
import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
+import com.tangem.common.routing.entity.AddressBookOpenMode
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.navigation.Route
@@ -174,8 +175,14 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class AddressBook(
- val predefinedAddress: String? = null,
- ) : AppRoute(path = "/address_book/predefinedAddress/$predefinedAddress")
+ val addressBookOpenMode: AddressBookOpenMode = AddressBookOpenMode.Default,
+ ) : AppRoute(
+ path = when (addressBookOpenMode) {
+ is AddressBookOpenMode.WithContactCreation ->
+ "/address_book/${addressBookOpenMode.address}-${addressBookOpenMode.networkId}"
+ AddressBookOpenMode.Default -> "/address_book"
+ },
+ )
@Serializable
data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") {
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/AddressBookOpenMode.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/AddressBookOpenMode.kt
new file mode 100644
index 0000000000..7c6b6206c4
--- /dev/null
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/AddressBookOpenMode.kt
@@ -0,0 +1,17 @@
+package com.tangem.common.routing.entity
+
+import kotlinx.serialization.Serializable
+
+/** How the address book is opened — part of the navigation contract, carried by [com.tangem.common.routing.AppRoute.AddressBook]. */
+@Serializable
+sealed interface AddressBookOpenMode {
+
+ @Serializable
+ data object Default : AddressBookOpenMode
+
+ @Serializable
+ data class WithContactCreation(
+ val address: String,
+ val networkId: String,
+ ) : AddressBookOpenMode
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt
index 35901d7a10..b9ffbb60e7 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/account/AccountIcon.kt
@@ -32,7 +32,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
enum class AccountIconSize {
- Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall
+ Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall, RedesignLarge
}
/**
@@ -132,6 +132,7 @@ fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: M
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
AccountIconSize.RedesignedDefault -> TangemTheme.typography2.headingSemibold28
AccountIconSize.RedesignExtraSmall -> TangemTheme.typography2.captionMedium11
+ AccountIconSize.RedesignLarge -> TangemTheme.typography3.heading.medium
}
val textSize by animateFloatAsState(
@@ -166,6 +167,7 @@ private fun AccountIconSize.iconSizeInDp(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 8.dp
AccountIconSize.RedesignedDefault -> 20.dp
AccountIconSize.RedesignExtraSmall -> 8.dp
+ AccountIconSize.RedesignLarge -> 32.dp
}
fun AccountIconSize.toBoxSize(): Dp = when (this) {
@@ -176,6 +178,7 @@ fun AccountIconSize.toBoxSize(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 14.dp
AccountIconSize.RedesignedDefault -> 40.dp
AccountIconSize.RedesignExtraSmall -> 16.dp
+ AccountIconSize.RedesignLarge -> 80.dp
}
private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
@@ -186,6 +189,7 @@ private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
AccountIconSize.ExtraSmall -> 4.dp
AccountIconSize.RedesignedDefault -> 12.dp
AccountIconSize.RedesignExtraSmall -> 6.dp
+ AccountIconSize.RedesignLarge -> 80.dp
}
@Preview(showBackground = true)
@@ -228,7 +232,8 @@ private fun Sample() {
AccountIconSize.Small -> AccountIconSize.ExtraSmall
AccountIconSize.ExtraSmall -> AccountIconSize.RedesignedDefault
AccountIconSize.RedesignedDefault -> AccountIconSize.RedesignExtraSmall
- AccountIconSize.RedesignExtraSmall -> AccountIconSize.Default
+ AccountIconSize.RedesignExtraSmall -> AccountIconSize.RedesignLarge
+ AccountIconSize.RedesignLarge -> AccountIconSize.Default
}
}) { Text("Change") }
diff --git a/core/ui/src/main/res/drawable/ic_address_book_24.xml b/core/ui/src/main/res/drawable/ic_address_book_24.xml
new file mode 100644
index 0000000000..ce54ca1a63
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_address_book_24.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_contact_20.xml b/core/ui/src/main/res/drawable/ic_contact_20.xml
deleted file mode 100644
index b6d762b21b..0000000000
--- a/core/ui/src/main/res/drawable/ic_contact_20.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/features/address-book/api/build.gradle.kts b/features/address-book/api/build.gradle.kts
index 425593dae2..50ede12ea4 100644
--- a/features/address-book/api/build.gradle.kts
+++ b/features/address-book/api/build.gradle.kts
@@ -10,6 +10,9 @@ android {
dependencies {
+ /* Project - Common */
+ api(projects.common.routing)
+
/* Project - Domain */
implementation(projects.domain.models)
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
index e9fe9ddb22..ff7f36e511 100644
--- 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
@@ -1,5 +1,6 @@
package com.tangem.features.addressbook
+import com.tangem.common.routing.entity.AddressBookOpenMode
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
@@ -7,5 +8,5 @@ interface AddressBookComponent : ComposableContentComponent {
interface Factory : ComponentFactory
- data class Params(val predefinedAddress: String?)
+ data class Params(val addressBookOpenMode: AddressBookOpenMode)
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/AddAddressComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/AddAddressComponent.kt
deleted file mode 100644
index 445dfa6c4a..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/AddAddressComponent.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.tangem.features.addressbook.addaddress
-
-import com.tangem.core.decompose.factory.ComponentFactory
-import com.tangem.core.ui.decompose.ComposableContentComponent
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
-
-internal interface AddAddressComponent : ComposableContentComponent {
-
- interface Factory : ComponentFactory
-
- data class Params(
- val onBackClick: () -> Unit,
- val onConfirm: (ValidatedAddress) -> Unit,
- )
-}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/DefaultAddAddressComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/DefaultAddAddressComponent.kt
index 374e522918..62d3e586a8 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/DefaultAddAddressComponent.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/DefaultAddAddressComponent.kt
@@ -7,16 +7,15 @@ 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.core.ui.decompose.ComposableContentComponent
import com.tangem.features.addressbook.addaddress.model.AddAddressModel
import com.tangem.features.addressbook.addaddress.ui.AddAddressContent
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
-internal class DefaultAddAddressComponent @AssistedInject constructor(
- @Assisted context: AppComponentContext,
- @Assisted params: AddAddressComponent.Params,
-) : AddAddressComponent, AppComponentContext by context {
+internal class DefaultAddAddressComponent(
+ appComponentContext: AppComponentContext,
+ params: Params,
+) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: AddAddressModel = getOrCreateModel(params)
@@ -30,11 +29,8 @@ internal class DefaultAddAddressComponent @AssistedInject constructor(
BackHandler(onBack = state.onBackClick)
}
- @AssistedFactory
- interface Factory : AddAddressComponent.Factory {
- override fun create(
- context: AppComponentContext,
- params: AddAddressComponent.Params,
- ): DefaultAddAddressComponent
- }
+ data class Params(
+ val onBackClick: () -> Unit,
+ val onConfirm: (ValidatedAddress) -> Unit,
+ )
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModel.kt
index c5ca0ebc21..925354b0d5 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModel.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModel.kt
@@ -1,30 +1,21 @@
package com.tangem.features.addressbook.addaddress.model
-import com.tangem.blockchainsdk.utils.toBlockchain
-import com.tangem.common.ui.extensions.iconResId
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.clipboard.ClipboardManager
-import com.tangem.core.ui.ds.button.TangemButtonType
-import com.tangem.core.ui.ds.button.TangemButtonUM
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrency
-import com.tangem.domain.models.network.Network
-import com.tangem.features.addressbook.addaddress.AddAddressComponent
-import com.tangem.features.addressbook.addaddress.contract.AddAddressUM
-import com.tangem.features.addressbook.addaddress.contract.AddressFieldUM
+import com.tangem.features.addressbook.addaddress.DefaultAddAddressComponent
+import com.tangem.features.addressbook.addaddress.state.AddAddressStateController
+import com.tangem.features.addressbook.addaddress.state.transformers.UpdateAddAddressInitialStateTransformer
+import com.tangem.features.addressbook.addaddress.state.transformers.UpdateAddressInputTransformer
+import com.tangem.features.addressbook.addaddress.state.transformers.UpdateAddressValidationTransformer
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import javax.inject.Inject
-import kotlin.collections.map
@OptIn(FlowPreview::class)
@ModelScoped
@@ -33,12 +24,12 @@ internal class AddAddressModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
multiAccountListSupplier: MultiAccountListSupplier,
private val clipboardManager: ClipboardManager,
+ private val stateController: AddAddressStateController,
) : Model() {
- private val params: AddAddressComponent.Params = paramsContainer.require()
+ private val params: DefaultAddAddressComponent.Params = paramsContainer.require()
- val state: StateFlow
- field = MutableStateFlow(getInitialState())
+ val state: StateFlow get() = stateController.uiState
private val availableCoins: StateFlow> = multiAccountListSupplier()
.map { accountLists ->
@@ -47,6 +38,7 @@ internal class AddAddressModel @Inject constructor(
.filterIsInstance()
.distinctBy { it.network.id }
}
+ .flowOn(dispatchers.default)
.stateIn(modelScope, SharingStarted.Eagerly, emptyList())
private val addressInput = state
@@ -55,94 +47,44 @@ internal class AddAddressModel @Inject constructor(
.debounce(ADD_ADDRESS_DEBOUNCE)
init {
- subscribeToAddressInput()
+ updateInitialState()
+ subscribeToAddressValidation()
}
- private fun onAddressChange(value: String, isPasted: Boolean = false) {
- state.update { oldState ->
- oldState.copy(
- addressField = oldState.addressField.copy(
- value = value,
- isValuePasted = isPasted,
- isError = false,
- error = null,
- ),
- chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Loading,
- )
- }
+ private fun updateInitialState() {
+ stateController.update(
+ UpdateAddAddressInitialStateTransformer(
+ onAddressChange = { onAddressChange(value = it) },
+ onAddressClear = { onAddressChange("") },
+ onPasteClick = ::onPaste,
+ onQrClick = { /* [REDACTED_TODO_COMMENT] */ },
+ onBackClick = params.onBackClick,
+ onConfirmClick = ::validateAndConfirm,
+ ),
+ )
}
- private fun subscribeToAddressInput() {
+ private fun onAddressChange(value: String) {
+ stateController.update(UpdateAddressInputTransformer(value = value))
+ }
+
+ private fun subscribeToAddressValidation() {
combine(addressInput, availableCoins) { input, coins ->
- getUniqueNetworks(input, coins)
+ UpdateAddressValidationTransformer(address = input, coins = coins)
}
- .onEach { availableNetworks ->
- state.update { oldState ->
- oldState.copy(
- availableNetworks = availableNetworks,
- chosenNetworkStateUM = createChosenNetworkState(availableNetworks),
- )
- }
- }
+ .onEach(stateController::update)
+ .flowOn(dispatchers.default)
.launchIn(modelScope)
}
- private fun createChosenNetworkState(availableNetworks: ImmutableList): AddAddressUM.ChosenNetworkStateUM {
- return if (availableNetworks.isEmpty()) {
- AddAddressUM.ChosenNetworkStateUM.Empty
- } else {
- AddAddressUM.ChosenNetworkStateUM.Result(
- networkUMList = availableNetworks
- .map { network ->
- AddAddressUM.ChosenNetworkStateUM.Result.NetworkUM(
- networkName = network.name,
- iconResId = network.iconResId,
- )
- }
- .toImmutableList(),
- )
- }
- }
-
- private fun getUniqueNetworks(input: String, coins: List): ImmutableList {
- return coins
- .filter { it.network.toBlockchain().validateAddress(input) }
- .map { it.network }
- .toImmutableList()
- }
-
private fun onPaste() {
- onAddressChange(value = clipboardManager.getText().orEmpty(), isPasted = true)
+ onAddressChange(value = clipboardManager.getText().orEmpty())
}
private fun validateAndConfirm() {
- // TODO([REDACTED_TASK_KEY]): validate the address and invoke params.onConfirm
+ // TODO Address book ([REDACTED_TASK_KEY]): navigate to the network-selection with the address and its matching networks.
}
- private fun getInitialState(): AddAddressUM = AddAddressUM(
- addressField = AddressFieldUM(
- value = "",
- placeholder = resourceReference(R.string.common_address),
- label = resourceReference(R.string.address_book_enter_address),
- isError = false,
- error = null,
- isValuePasted = false,
- ),
- availableNetworks = persistentListOf(),
- buttonUM = TangemButtonUM(
- text = TextReference.Res(R.string.address_book_add_address),
- type = TangemButtonType.Primary,
- isEnabled = false,
- onClick = ::validateAndConfirm,
- ),
- chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Empty,
- onAddressChange = { onAddressChange(value = it) },
- onAddressClear = { onAddressChange("") },
- onPasteClick = ::onPaste,
- onQrClick = { /* [REDACTED_TODO_COMMENT] */ },
- onBackClick = params.onBackClick,
- )
-
companion object {
private const val ADD_ADDRESS_DEBOUNCE = 500L
}
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/AddAddressStateController.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/AddAddressStateController.kt
new file mode 100644
index 0000000000..d5ecedc0de
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/AddAddressStateController.kt
@@ -0,0 +1,50 @@
+package com.tangem.features.addressbook.addaddress.state
+
+import com.tangem.core.decompose.di.ModelScoped
+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.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.features.addressbook.addaddress.ui.state.AddressFieldUM
+import com.tangem.utils.transformer.Transformer
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@ModelScoped
+internal class AddAddressStateController @Inject constructor() {
+
+ private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState())
+
+ val uiState: StateFlow get() = mutableUiState.asStateFlow()
+
+ fun update(transformer: Transformer) {
+ mutableUiState.update(function = transformer::transform)
+ }
+
+ private fun getInitialState(): AddAddressUM = AddAddressUM(
+ addressField = AddressFieldUM(
+ value = "",
+ placeholder = resourceReference(R.string.address_book_enter_address),
+ label = resourceReference(R.string.common_address),
+ isError = false,
+ ),
+ buttonUM = TangemButtonUM(
+ text = TextReference.Res(R.string.address_book_add_address),
+ type = TangemButtonType.Primary,
+ isEnabled = false,
+ onClick = {},
+ ),
+ chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Empty,
+ onAddressChange = {},
+ onAddressClear = {},
+ onPasteClick = {},
+ onQrClick = {},
+ onBackClick = {},
+ onNetworkClick = {},
+ )
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddAddressInitialStateTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddAddressInitialStateTransformer.kt
new file mode 100644
index 0000000000..15007b6655
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddAddressInitialStateTransformer.kt
@@ -0,0 +1,29 @@
+package com.tangem.features.addressbook.addaddress.state.transformers
+
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Wires the callbacks owned by [com.tangem.features.addressbook.addaddress.model.AddAddressModel] into the initial
+ * state produced by [com.tangem.features.addressbook.addaddress.state.AddAddressStateController].
+ */
+internal class UpdateAddAddressInitialStateTransformer(
+ private val onAddressChange: (String) -> Unit,
+ private val onAddressClear: () -> Unit,
+ private val onPasteClick: () -> Unit,
+ private val onQrClick: () -> Unit,
+ private val onBackClick: () -> Unit,
+ private val onConfirmClick: () -> Unit,
+) : Transformer {
+
+ override fun transform(prevState: AddAddressUM): AddAddressUM {
+ return prevState.copy(
+ onAddressChange = onAddressChange,
+ onAddressClear = onAddressClear,
+ onPasteClick = onPasteClick,
+ onQrClick = onQrClick,
+ onBackClick = onBackClick,
+ buttonUM = prevState.buttonUM.copy(onClick = onConfirmClick),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressInputTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressInputTransformer.kt
new file mode 100644
index 0000000000..f9b84f065e
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressInputTransformer.kt
@@ -0,0 +1,25 @@
+package com.tangem.features.addressbook.addaddress.state.transformers
+
+import com.tangem.core.ui.R
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Updates the address field with a freshly entered/pasted [value] and clears any previous error, restoring the default
+ * label. The actual (re)validation runs after a debounce — see [UpdateAddressValidationTransformer].
+ */
+internal class UpdateAddressInputTransformer(
+ private val value: String,
+) : Transformer {
+
+ override fun transform(prevState: AddAddressUM): AddAddressUM {
+ return prevState.copy(
+ addressField = prevState.addressField.copy(
+ value = value,
+ isError = false,
+ label = resourceReference(R.string.common_address),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressValidationTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressValidationTransformer.kt
new file mode 100644
index 0000000000..6d7c469965
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/state/transformers/UpdateAddressValidationTransformer.kt
@@ -0,0 +1,36 @@
+package com.tangem.features.addressbook.addaddress.state.transformers
+
+import com.tangem.blockchainsdk.utils.toBlockchain
+import com.tangem.core.ui.R
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Validates [address] against the wallet's [coins] and reflects the result in the UI.
+ *
+ * The network is not chosen on this screen (it is selected on the next screen), so the address is valid when it matches
+ * at least one of the available networks — the same blockchain check the Send flow uses. An invalid (non-empty,
+ * matching nothing) address surfaces the error in the field label and disables the confirm button.
+ */
+internal class UpdateAddressValidationTransformer(
+ private val address: String,
+ private val coins: List,
+) : Transformer {
+
+ override fun transform(prevState: AddAddressUM): AddAddressUM {
+ val hasMatchedAnyNetwork = address.isNotBlank() &&
+ coins.any { it.network.toBlockchain().validateAddress(address) }
+ val isError = address.isNotBlank() && !hasMatchedAnyNetwork
+ val label = if (isError) {
+ resourceReference(R.string.address_book_invalid_address_error)
+ } else {
+ resourceReference(R.string.common_address)
+ }
+ return prevState.copy(
+ addressField = prevState.addressField.copy(isError = isError, label = label),
+ buttonUM = prevState.buttonUM.copy(isEnabled = hasMatchedAnyNetwork),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/AddAddressContent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/AddAddressContent.kt
index 9cf8321120..6a8d9f5cfe 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/AddAddressContent.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/AddAddressContent.kt
@@ -3,14 +3,15 @@ package com.tangem.features.addressbook.addaddress.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
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 com.tangem.core.ui.R
-import com.tangem.core.ui.components.SpacerH12
-import com.tangem.core.ui.ds.button.PrimaryTangemButton
+import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
@@ -20,9 +21,8 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
-import com.tangem.features.addressbook.addaddress.contract.AddAddressUM
-import com.tangem.features.addressbook.addaddress.contract.AddressFieldUM
-import kotlinx.collections.immutable.persistentListOf
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.features.addressbook.addaddress.ui.state.AddressFieldUM
@Composable
internal fun AddAddressContent(state: AddAddressUM, modifier: Modifier = Modifier) {
@@ -34,7 +34,6 @@ internal fun AddAddressContent(state: AddAddressUM, modifier: Modifier = Modifie
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemTopBar(
- modifier = Modifier.statusBarsPadding(),
title = resourceReference(R.string.address_book_add_address),
startContent = {
TangemButton(
@@ -47,14 +46,23 @@ internal fun AddAddressContent(state: AddAddressUM, modifier: Modifier = Modifie
)
RecipientRow(
+ modifier = Modifier.padding(horizontal = 16.dp),
addressField = state.addressField,
onValueChange = state.onAddressChange,
onAddressClear = state.onAddressClear,
onQrClick = state.onQrClick,
onPasteClick = state.onPasteClick,
)
- SpacerH12()
- NetworkBlock(state.chosenNetworkStateUM)
+ SpacerH(20.dp)
+ NetworkBlock(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .fillMaxWidth()
+ .background(color = TangemTheme.colors3.bg.secondary),
+ chosenNetworkStateUM = state.chosenNetworkStateUM,
+ onNetworkSelectClick = state.onNetworkClick,
+ )
PrimaryButton(state.buttonUM)
}
}
@@ -62,13 +70,15 @@ internal fun AddAddressContent(state: AddAddressUM, modifier: Modifier = Modifie
@Composable
private fun ColumnScope.PrimaryButton(buttonUM: TangemButtonUM) {
Spacer(modifier = Modifier.weight(1f))
-
- PrimaryTangemButton(
+ TangemButton(
modifier = Modifier
.fillMaxWidth()
- .navigationBarsPadding()
- .padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
- buttonUM = buttonUM,
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ onClick = buttonUM.onClick,
+ isEnabled = buttonUM.isEnabled,
+ isLoading = buttonUM.isLoading,
+ size = TangemButton.Size.X12,
+ text = buttonUM.text,
)
}
@@ -84,7 +94,6 @@ private fun Preview_AddAddressContent() {
placeholder = resourceReference(R.string.address_book_enter_address),
label = resourceReference(R.string.common_address),
),
- availableNetworks = persistentListOf(),
buttonUM = TangemButtonUM(
text = TextReference.Res(R.string.address_book_add_address),
type = TangemButtonType.Primary,
@@ -97,6 +106,7 @@ private fun Preview_AddAddressContent() {
onPasteClick = {},
onQrClick = {},
onBackClick = {},
+ onNetworkClick = {},
),
)
}
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/NetworkBlock.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/NetworkBlock.kt
index dc91b1d8b7..e2b3e1b6a5 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/NetworkBlock.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/NetworkBlock.kt
@@ -1,86 +1,95 @@
package com.tangem.features.addressbook.addaddress.ui
-import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
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.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.ds2.loader.TangemLoader
+import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
+import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
-import com.tangem.features.addressbook.addaddress.contract.AddAddressUM
-import com.tangem.features.addressbook.addaddress.contract.AddAddressUM.ChosenNetworkStateUM.Result.NetworkUM
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM
+import com.tangem.features.addressbook.addaddress.ui.state.AddAddressUM.ChosenNetworkStateUM.Result.NetworkUM
+import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
private const val MAX_VISIBLE_NETWORKS = 3
-private val NetworkIconSize = 24.dp
// Horizontal advance per icon. Smaller than the icon size so icons overlap; the bg-colored ring on
// the icon drawn on top carves the crescent cut-out from the icon below.
private val NetworkIconStep = 18.dp
@Composable
-internal fun NetworkBlock(chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM) {
+internal fun NetworkBlock(
+ onNetworkSelectClick: () -> Unit,
+ chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM,
+ modifier: Modifier = Modifier,
+) {
TangemRow(
verticalAlignment = TangemRowVerticalAlignment.Center,
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .clip(RoundedCornerShape(16.dp))
- .fillMaxWidth()
- .background(color = TangemTheme.colors3.bg.secondary)
- .padding(horizontal = 4.dp),
+ modifier = modifier,
titleSlot = {
Text(
text = stringResourceSafe(R.string.common_network),
- style = TangemTheme.typography.body2,
+ style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
},
endSlot = {
- SelectNetworkButton(chosenNetworkStateUM)
+ SelectNetworkButton(
+ onNetworkSelectClick = onNetworkSelectClick,
+ chosenNetworkStateUM = chosenNetworkStateUM,
+ )
},
)
}
@Composable
-private fun SelectNetworkButton(chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM) {
+private fun SelectNetworkButton(
+ onNetworkSelectClick: () -> Unit,
+ chosenNetworkStateUM: AddAddressUM.ChosenNetworkStateUM,
+) {
Row(
+ modifier = Modifier.clickableSingle(
+ onClick = onNetworkSelectClick,
+ enabled = chosenNetworkStateUM !is AddAddressUM.ChosenNetworkStateUM.Loading,
+ ),
verticalAlignment = Alignment.CenterVertically,
) {
when (chosenNetworkStateUM) {
is AddAddressUM.ChosenNetworkStateUM.Result -> NetworkIconsResolver(chosenNetworkStateUM.networkUMList)
- AddAddressUM.ChosenNetworkStateUM.Loading -> TangemLoader()
+ AddAddressUM.ChosenNetworkStateUM.Loading -> TangemLoader(size = TangemLoaderSize.X20)
AddAddressUM.ChosenNetworkStateUM.Empty -> {
Text(
modifier = Modifier.padding(start = 8.dp),
text = stringResourceSafe(R.string.address_book_select_network),
- style = TangemTheme.typography.body2,
+ style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
)
+ SpacerW(4.dp)
ChevronIcon()
}
}
@@ -100,7 +109,7 @@ private fun NetworkIconsResolver(networks: ImmutableList) {
Text(
modifier = Modifier.padding(start = 8.dp),
text = network.networkName,
- style = TangemTheme.typography.body2,
+ style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
)
ChevronIcon()
@@ -120,7 +129,7 @@ private fun OverlappingNetworkIcons(networks: ImmutableList) {
val remaining = networks.size - visible.size
Box(modifier = Modifier.wrapContentWidth()) {
- visible.forEachIndexed { index, network ->
+ visible.fastForEachIndexed { index, network ->
Image(
painter = painterResource(id = network.iconResId),
contentDescription = null,
@@ -128,7 +137,7 @@ private fun OverlappingNetworkIcons(networks: ImmutableList) {
modifier = Modifier
.padding(start = NetworkIconStep * index)
.networkIconRing()
- .size(NetworkIconSize),
+ .size(24.dp),
)
}
if (remaining > 0) {
@@ -137,12 +146,13 @@ private fun OverlappingNetworkIcons(networks: ImmutableList) {
.padding(start = NetworkIconStep * visible.size)
.networkIconRing()
.background(color = TangemTheme.colors3.bg.tertiary)
- .size(NetworkIconSize),
+ .heightIn(min = 24.dp)
+ .padding(vertical = 2.dp, horizontal = 4.dp),
contentAlignment = Alignment.Center,
) {
Text(
- text = "+$remaining",
- style = TangemTheme.typography.caption1,
+ text = "${StringsSigns.PLUS}$remaining",
+ style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
@@ -160,20 +170,23 @@ private fun Modifier.networkIconRing(): Modifier = this
@Composable
private fun ChevronIcon() {
- Image(
- modifier = Modifier.padding(start = 8.dp),
- painter = painterResource(id = R.drawable.ic_select_18_24),
+ Icon(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(20.dp),
+ tint = TangemTheme.colors3.icon.secondary,
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
)
}
-@Preview
-@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Preview(showBackground = true)
@Composable
private fun Preview_NetworkBlock() {
TangemThemePreviewRedesign {
Column {
NetworkBlock(
+ onNetworkSelectClick = {},
chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Result(
networkUMList = persistentListOf(
NetworkUM(networkName = "Ethereum", iconResId = R.drawable.img_eth_22),
@@ -182,6 +195,7 @@ private fun Preview_NetworkBlock() {
)
SpacerH12()
NetworkBlock(
+ onNetworkSelectClick = {},
chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Result(
networkUMList = persistentListOf(
NetworkUM(networkName = "Ethereum", iconResId = R.drawable.img_eth_22),
@@ -192,6 +206,7 @@ private fun Preview_NetworkBlock() {
)
SpacerH12()
NetworkBlock(
+ onNetworkSelectClick = {},
chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Result(
networkUMList = List(15) {
NetworkUM(networkName = "Network", iconResId = R.drawable.img_eth_22)
@@ -199,9 +214,9 @@ private fun Preview_NetworkBlock() {
),
)
SpacerH12()
- NetworkBlock(chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Loading)
+ NetworkBlock(chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Loading, onNetworkSelectClick = {})
SpacerH12()
- NetworkBlock(chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Empty)
+ NetworkBlock(chosenNetworkStateUM = AddAddressUM.ChosenNetworkStateUM.Empty, onNetworkSelectClick = {})
}
}
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/RecipientRow.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/RecipientRow.kt
index 8ebe82cc0f..aadf4f91ca 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/RecipientRow.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/RecipientRow.kt
@@ -3,11 +3,7 @@ package com.tangem.features.addressbook.addaddress.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
@@ -19,7 +15,6 @@ 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.SpacerH12
-import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
@@ -28,13 +23,14 @@ import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowContentLead
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_cross_circle_20_filled
-import com.tangem.features.addressbook.addaddress.contract.AddressFieldUM
+import com.tangem.core.ui.res.generated.icons.ic_scan_20
+import com.tangem.features.addressbook.addaddress.ui.state.AddressFieldUM
@Composable
internal fun RecipientRow(
@@ -43,19 +39,23 @@ internal fun RecipientRow(
onAddressClear: () -> Unit,
onQrClick: () -> Unit,
onPasteClick: () -> Unit,
+ modifier: Modifier = Modifier,
) {
Column(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .clip(RoundedCornerShape(16.dp))
+ modifier = modifier
+ .clip(RoundedCornerShape(24.dp))
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
Text(
- modifier = Modifier.padding(start = 16.dp, top = 16.dp),
- text = stringResourceSafe(R.string.common_address),
- style = TangemTheme.typography.caption1,
- color = TangemTheme.colors3.text.secondary,
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 4.dp),
+ text = addressField.label.resolveReference(),
+ style = TangemTheme.typography3.caption.medium,
+ color = if (addressField.isError) {
+ TangemTheme.colors3.text.status.error
+ } else {
+ TangemTheme.colors3.text.secondary
+ },
)
TangemRow(
modifier = Modifier.fillMaxWidth(),
@@ -64,7 +64,7 @@ internal fun RecipientRow(
startSlot = {
TangemIcon(
modifier = Modifier
- .size(36.dp)
+ .size(40.dp)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.tertiary),
tangemIconUM = TangemIconUM.Ident(text = addressField.value),
@@ -72,43 +72,55 @@ internal fun RecipientRow(
},
titleSlot = {
SimpleTextField(
- modifier = Modifier
- .weight(1f)
- .padding(start = 12.dp),
+ modifier = Modifier.weight(1f),
value = addressField.value,
onValueChange = onValueChange,
- placeholder = TextReference.Res(R.string.address_book_enter_address),
- singleLine = false,
+ placeholder = addressField.placeholder,
)
},
endSlot = {
- if (addressField.value.isNotEmpty()) {
- Icon(
- modifier = Modifier.clickable(onClick = onAddressClear),
- imageVector = Icons.ic_cross_circle_20_filled,
- tint = TangemTheme.colors3.icon.tertiary,
- contentDescription = null,
- )
- } else {
- Row {
- TangemButton(
- variant = TangemButton.Variant.Secondary,
- iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_qrcode_scaner_24),
- onClick = onQrClick,
- )
- SpacerW8()
- TangemButton(
- variant = TangemButton.Variant.Primary,
- text = TextReference.Res(id = R.string.common_paste),
- onClick = onPasteClick,
- )
- }
- }
+ RecipientEndSlot(
+ hasValue = addressField.value.isNotEmpty(),
+ onAddressClear = onAddressClear,
+ onQrClick = onQrClick,
+ onPasteClick = onPasteClick,
+ )
},
)
}
}
+@Composable
+private fun RecipientEndSlot(
+ hasValue: Boolean,
+ onAddressClear: () -> Unit,
+ onQrClick: () -> Unit,
+ onPasteClick: () -> Unit,
+) {
+ if (hasValue) {
+ Icon(
+ modifier = Modifier
+ .clip(CircleShape)
+ .clickable(onClick = onAddressClear),
+ imageVector = Icons.ic_cross_circle_20_filled,
+ tint = TangemTheme.colors3.icon.tertiary,
+ contentDescription = null,
+ )
+ } else {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TangemButton(
+ variant = TangemButton.Variant.Secondary,
+ iconStart = TangemIconUM.Icon(imageVector = Icons.ic_scan_20),
+ onClick = onQrClick,
+ )
+ TangemButton(
+ text = TextReference.Res(id = R.string.common_paste),
+ onClick = onPasteClick,
+ )
+ }
+ }
+}
+
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddAddressUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddAddressUM.kt
similarity index 62%
rename from features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddAddressUM.kt
rename to features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddAddressUM.kt
index d54c5e1716..45704c2355 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddAddressUM.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddAddressUM.kt
@@ -1,14 +1,13 @@
-package com.tangem.features.addressbook.addaddress.contract
+package com.tangem.features.addressbook.addaddress.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.button.TangemButtonUM
-import com.tangem.domain.models.network.Network
import kotlinx.collections.immutable.ImmutableList
+@Immutable
internal data class AddAddressUM(
val addressField: AddressFieldUM,
- val availableNetworks: ImmutableList,
val buttonUM: TangemButtonUM,
val chosenNetworkStateUM: ChosenNetworkStateUM,
val onAddressChange: (String) -> Unit,
@@ -16,15 +15,14 @@ internal data class AddAddressUM(
val onPasteClick: () -> Unit,
val onQrClick: () -> Unit,
val onBackClick: () -> Unit,
+ val onNetworkClick: () -> Unit,
) {
@Immutable
- sealed class ChosenNetworkStateUM {
- data object Loading : ChosenNetworkStateUM()
- data object Empty : ChosenNetworkStateUM()
- data class Result(
- val networkUMList: ImmutableList,
- ) : ChosenNetworkStateUM() {
+ sealed interface ChosenNetworkStateUM {
+ data object Loading : ChosenNetworkStateUM
+ data object Empty : ChosenNetworkStateUM
+ data class Result(val networkUMList: ImmutableList) : ChosenNetworkStateUM {
data class NetworkUM(
val networkName: String,
@DrawableRes val iconResId: Int,
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddressFieldUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddressFieldUM.kt
similarity index 54%
rename from features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddressFieldUM.kt
rename to features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddressFieldUM.kt
index ea8e324bd7..2e65e1b381 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/contract/AddressFieldUM.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/addaddress/ui/state/AddressFieldUM.kt
@@ -1,4 +1,4 @@
-package com.tangem.features.addressbook.addaddress.contract
+package com.tangem.features.addressbook.addaddress.ui.state
import com.tangem.core.ui.extensions.TextReference
@@ -7,7 +7,4 @@ internal data class AddressFieldUM(
val placeholder: TextReference,
val label: TextReference,
val isError: Boolean = false,
- val error: TextReference? = null,
- val isValuePasted: Boolean = false,
- val blockchainAddress: String? = null,
)
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookChildFactory.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookChildFactory.kt
new file mode 100644
index 0000000000..3283d11e76
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookChildFactory.kt
@@ -0,0 +1,56 @@
+package com.tangem.features.addressbook.common
+
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.domain.addressbook.model.ContactId
+import com.tangem.features.addressbook.addaddress.DefaultAddAddressComponent
+import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
+import com.tangem.features.addressbook.list.DefaultAddressBookListComponent
+import com.tangem.features.addressbook.route.AddressBookRoute
+import kotlinx.collections.immutable.persistentListOf
+import javax.inject.Inject
+
+/**
+ * Builds the child screens of the address book feature for a given [AddressBookRoute], wiring their callbacks to the
+ * container's [AddressBookClickIntents]. Mirrors the `FeedEntryChildFactory` pattern used by the feed feature.
+ */
+internal class AddressBookChildFactory @Inject constructor() {
+
+ fun createChild(
+ route: AddressBookRoute,
+ context: AppComponentContext,
+ clickIntents: AddressBookClickIntents,
+ ): ComposableContentComponent = when (route) {
+ AddressBookRoute.List -> DefaultAddressBookListComponent(
+ appComponentContext = context,
+ params = DefaultAddressBookListComponent.Params(
+ onContactClick = { clickIntents.onContactClick(ContactId(it)) },
+ onAddContactClick = clickIntents::onAddContactClick,
+ ),
+ )
+ is AddressBookRoute.EditContact -> DefaultEditContactComponent(
+ appComponentContext = context,
+ params = DefaultEditContactComponent.Params(
+ contactId = route.contactId?.let(::ContactId),
+ predefinedAddress = buildPredefinedAddress(route),
+ onBackClick = clickIntents::onEditContactBack,
+ onAddAddressClick = clickIntents::onAddAddressClick,
+ ),
+ )
+ AddressBookRoute.AddAddress -> DefaultAddAddressComponent(
+ appComponentContext = context,
+ params = DefaultAddAddressComponent.Params(
+ onBackClick = clickIntents::onAddAddressBack,
+ onConfirm = clickIntents::onAddressConfirmed,
+ ),
+ )
+ }
+
+ /** Builds the address attached up-front in WithContactCreation mode, when both the address and network are known. */
+ private fun buildPredefinedAddress(route: AddressBookRoute.EditContact): ValidatedAddress? {
+ val address = route.predefinedAddress ?: return null
+ val networkId = route.predefinedNetworkId ?: return null
+ return ValidatedAddress(address = address, networkIds = persistentListOf(networkId))
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookClickIntents.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookClickIntents.kt
new file mode 100644
index 0000000000..5b13a5204f
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookClickIntents.kt
@@ -0,0 +1,26 @@
+package com.tangem.features.addressbook.common
+
+import com.tangem.domain.addressbook.model.ContactId
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
+
+/**
+ * Navigation/click contract that the container ([DefaultAddressBookComponent]) implements and passes down to its
+ * children through [AddressBookChildFactory]. Keeping all cross-screen intents in one place removes the need for the
+ * children to know about each other or about navigation.
+ *
+ * Result delivery (the confirmed address) is handled out-of-band by [AddressBookResultHolder], not by this contract.
+ */
+internal interface AddressBookClickIntents {
+
+ fun onContactClick(contactId: ContactId)
+
+ fun onAddContactClick()
+
+ fun onEditContactBack()
+
+ fun onAddAddressClick()
+
+ fun onAddAddressBack()
+
+ fun onAddressConfirmed(address: ValidatedAddress)
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookResultHolder.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookResultHolder.kt
new file mode 100644
index 0000000000..1ba1423a22
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/AddressBookResultHolder.kt
@@ -0,0 +1,29 @@
+package com.tangem.features.addressbook.common
+
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Carries a [ValidatedAddress] confirmed on the AddAddress screen over to the EditContact screen.
+ *
+ * The two screens live in independent model scopes, so a shared singleton holder is used to hand the result over
+ * instead of routing it through navigation/click intents. The producer calls [setConfirmedAddress]; the consumer
+ * observes [confirmedAddress] and calls [clear] after applying the value so it is not re-applied on resubscription.
+ */
+@Singleton
+internal class AddressBookResultHolder @Inject constructor() {
+
+ val confirmedAddress: StateFlow
+ field = MutableStateFlow(null)
+
+ fun setConfirmedAddress(address: ValidatedAddress) {
+ confirmedAddress.value = address
+ }
+
+ fun clear() {
+ confirmedAddress.value = null
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt
new file mode 100644
index 0000000000..462aec3631
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookComponent.kt
@@ -0,0 +1,117 @@
+package com.tangem.features.addressbook.common
+
+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.slide
+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.common.routing.entity.AddressBookOpenMode
+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.editcontact.ui.state.ValidatedAddress
+import com.tangem.features.addressbook.route.AddressBookRoute
+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 childFactory: AddressBookChildFactory,
+ private val resultHolder: AddressBookResultHolder,
+) : AddressBookComponent, AppComponentContext by context {
+
+ private val navigation = StackNavigation()
+
+ init {
+ // Drop any address left over from a previous session before the (possibly preloaded) stack starts collecting.
+ resultHolder.clear()
+ }
+
+ private val clickIntents = object : AddressBookClickIntents {
+
+ override fun onContactClick(contactId: ContactId) {
+ navigation.pushNew(AddressBookRoute.EditContact(contactId = contactId.value))
+ }
+
+ override fun onAddContactClick() {
+ navigation.pushNew(AddressBookRoute.EditContact())
+ }
+
+ override fun onEditContactBack() {
+ navigation.pop()
+ }
+
+ override fun onAddAddressClick() {
+ navigation.pushNew(AddressBookRoute.AddAddress)
+ }
+
+ override fun onAddAddressBack() {
+ navigation.pop()
+ }
+
+ override fun onAddressConfirmed(address: ValidatedAddress) {
+ resultHolder.setConfirmedAddress(address)
+ navigation.pop()
+ }
+ }
+
+ private val contentStack = childStack(
+ key = "address_book_stack",
+ source = navigation,
+ serializer = AddressBookRoute.serializer(),
+ initialStack = ::initialStack,
+ handleBackButton = false,
+ childFactory = ::screenChild,
+ )
+
+ @Composable
+ override fun Content(modifier: Modifier) {
+ val childStack by contentStack.subscribeAsState()
+ Children(
+ modifier = modifier,
+ stack = childStack,
+ animation = stackAnimation(slide()),
+ ) { child ->
+ child.instance.Content(Modifier)
+ }
+ }
+
+ private fun screenChild(config: AddressBookRoute, componentContext: ComponentContext): ComposableContentComponent {
+ return childFactory.createChild(
+ route = config,
+ context = childByContext(componentContext),
+ clickIntents = clickIntents,
+ )
+ }
+
+ private fun initialStack(): List = when (val mode = params.addressBookOpenMode) {
+ AddressBookOpenMode.Default -> listOf(AddressBookRoute.List)
+ is AddressBookOpenMode.WithContactCreation -> listOf(
+ AddressBookRoute.List,
+ // Address + network are already known, so open the new contact with that address attached — no AddAddress.
+ AddressBookRoute.EditContact(
+ predefinedAddress = mode.address,
+ predefinedNetworkId = mode.networkId,
+ ),
+ )
+ }
+
+ @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/DefaultAddressBookFeatureToggles.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookFeatureToggles.kt
similarity index 78%
rename from features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/DefaultAddressBookFeatureToggles.kt
rename to features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookFeatureToggles.kt
index 08247d086c..c3ddb2f1b9 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/DefaultAddressBookFeatureToggles.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/common/DefaultAddressBookFeatureToggles.kt
@@ -1,7 +1,8 @@
-package com.tangem.features.addressbook
+package com.tangem.features.addressbook.common
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
+import com.tangem.features.addressbook.AddressBookFeatureToggles
internal class DefaultAddressBookFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
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
deleted file mode 100644
index 815ebd2498..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.tangem.features.addressbook.component
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-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()
-
- @Serializable
- data object AddAddress : 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
deleted file mode 100644
index e843ffd769..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.tangem.features.addressbook.component
-
-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.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.addaddress.AddAddressComponent
-import com.tangem.features.addressbook.editcontact.EditContactComponent
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
-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,
- private val editContactComponentFactory: EditContactComponent.Factory,
- private val addAddressComponentFactory: AddAddressComponent.Factory,
-) : AddressBookComponent, AppComponentContext by context {
-
- private val navigation = StackNavigation()
-
- /**
- * Consumer for the address entered on the [AddressBookRoute.AddAddress] screen, registered by the EditContact
- * screen when it requests adding an address and invoked when AddAddress confirms. Transient by design — the
- * entered addresses live only in EditContact's in-memory state until the contact is saved.
- */
- private var pendingAddressSink: ((ValidatedAddress) -> Unit)? = null
-
- 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()
-
- 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(
- onContactClick = { contactId ->
- 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() },
- onAddAddressClick = { onResult ->
- pendingAddressSink = onResult
- navigation.pushNew(AddressBookRoute.AddAddress)
- },
- ),
- )
- AddressBookRoute.AddAddress -> addAddressComponentFactory.create(
- context = childByContext(componentContext),
- params = AddAddressComponent.Params(
- onBackClick = {
- pendingAddressSink = null
- navigation.pop()
- },
- onConfirm = { address ->
- pendingAddressSink?.invoke(address)
- pendingAddressSink = null
- navigation.pop()
- },
- ),
- )
- }
-
- @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
index 493422e7c2..868ed604e7 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
@@ -1,13 +1,7 @@
package com.tangem.features.addressbook.di
import com.tangem.features.addressbook.AddressBookComponent
-import com.tangem.features.addressbook.addaddress.AddAddressComponent
-import com.tangem.features.addressbook.addaddress.DefaultAddAddressComponent
-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 com.tangem.features.addressbook.common.DefaultAddressBookComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -21,18 +15,4 @@ internal interface AddressBookComponentModule {
@Binds
@Singleton
fun bindAddressBookComponentFactory(factory: DefaultAddressBookComponent.Factory): AddressBookComponent.Factory
-
- @Binds
- @Singleton
- fun bindAddressBookListComponentFactory(
- factory: DefaultAddressBookListComponent.Factory,
- ): AddressBookListComponent.Factory
-
- @Binds
- @Singleton
- fun bindEditContactComponentFactory(factory: DefaultEditContactComponent.Factory): EditContactComponent.Factory
-
- @Binds
- @Singleton
- fun bindAddAddressComponentFactory(factory: DefaultAddAddressComponent.Factory): AddAddressComponent.Factory
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModule.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModule.kt
index 4594968b09..3ca882b9ec 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModule.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModule.kt
@@ -2,7 +2,7 @@ package com.tangem.features.addressbook.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.addressbook.AddressBookFeatureToggles
-import com.tangem.features.addressbook.DefaultAddressBookFeatureToggles
+import com.tangem.features.addressbook.common.DefaultAddressBookFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
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
index 8f83105d52..7fa44a861e 100644
--- 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
@@ -7,16 +7,16 @@ 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.core.ui.decompose.ComposableContentComponent
+import com.tangem.domain.addressbook.model.ContactId
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
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
-internal class DefaultEditContactComponent @AssistedInject constructor(
- @Assisted context: AppComponentContext,
- @Assisted params: EditContactComponent.Params,
-) : EditContactComponent, AppComponentContext by context {
+internal class DefaultEditContactComponent(
+ appComponentContext: AppComponentContext,
+ params: Params,
+) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: EditContactModel = getOrCreateModel(params)
@@ -30,11 +30,10 @@ internal class DefaultEditContactComponent @AssistedInject constructor(
BackHandler(onBack = state.onCloseClick)
}
- @AssistedFactory
- interface Factory : EditContactComponent.Factory {
- override fun create(
- context: AppComponentContext,
- params: EditContactComponent.Params,
- ): DefaultEditContactComponent
- }
+ data class Params(
+ val contactId: ContactId?,
+ val predefinedAddress: ValidatedAddress? = null,
+ val onBackClick: () -> Unit,
+ val onAddAddressClick: () -> Unit,
+ )
}
\ 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
deleted file mode 100644
index 2e93f5ff24..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-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
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
-
-internal interface EditContactComponent : ComposableContentComponent {
-
- interface Factory : ComponentFactory
-
- data class Params(
- val contactId: ContactId?,
- val onBackClick: () -> Unit,
- val onAddAddressClick: (onResult: (ValidatedAddress) -> Unit) -> Unit,
- )
-}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/ValidatedAddress.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/ValidatedAddress.kt
deleted file mode 100644
index 87a094ee60..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/ValidatedAddress.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.tangem.features.addressbook.editcontact.contract
-
-import com.tangem.domain.models.network.Network
-
-/**
- * A recipient address that has been validated and resolved to a [Network] on the AddAddress screen.
- *
- * This is the in-progress (pre-save) representation accumulated in [EditContactUM]. It is converted to a domain
- * `AddressEntry` only when the contact is persisted, since the entry's id and signature are produced at save time.
- */
-data class ValidatedAddress(
- val address: String,
- val network: Network,
-)
\ 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
index d4bcdf27ab..90f414fabf 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt
@@ -1,80 +1,79 @@
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.features.addressbook.editcontact.contract.ValidatedAddress
+import com.tangem.features.addressbook.common.AddressBookResultHolder
+import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
+import com.tangem.features.addressbook.editcontact.state.EditContactStateController
+import com.tangem.features.addressbook.editcontact.state.transformers.AddValidatedAddressTransformer
+import com.tangem.features.addressbook.editcontact.state.transformers.SelectContactColorTransformer
+import com.tangem.features.addressbook.editcontact.state.transformers.UpdateContactNameTransformer
+import com.tangem.features.addressbook.editcontact.state.transformers.UpdateEditContactInitialStateTransformer
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@ModelScoped
internal class EditContactModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
+ private val stateController: EditContactStateController,
+ private val resultHolder: AddressBookResultHolder,
) : Model() {
- private val params: EditContactComponent.Params = paramsContainer.require()
+ private val params: DefaultEditContactComponent.Params = paramsContainer.require()
- val state: StateFlow
- field = MutableStateFlow(getInitialState())
+ val state: StateFlow get() = stateController.uiState
+
+ init {
+ updateInitialState()
+ prefillPredefinedAddress()
+ subscribeToConfirmedAddresses()
+ }
+
+ /** In WithContactCreation mode the contact opens with the already-known address attached. */
+ private fun prefillPredefinedAddress() {
+ params.predefinedAddress?.let(::addAddress)
+ }
+
+ private fun updateInitialState() {
+ stateController.update(
+ UpdateEditContactInitialStateTransformer(
+ isExistingContact = params.contactId != null,
+ onNameChange = ::onNameChange,
+ onColorSelect = ::onColorSelect,
+ onCloseClick = params.onBackClick,
+ onAddAddressClick = params.onAddAddressClick,
+ ),
+ )
+ }
+
+ private fun subscribeToConfirmedAddresses() {
+ resultHolder.confirmedAddress
+ .filterNotNull()
+ .onEach { address ->
+ addAddress(address)
+ resultHolder.clear()
+ }
+ .launchIn(modelScope)
+ }
private fun onNameChange(name: String) {
- state.update { it.copy(name = name) }
+ stateController.update(UpdateContactNameTransformer(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 requestAddAddress() {
- params.onAddAddressClick(::addAddress)
+ stateController.update(SelectContactColorTransformer(color = color))
}
private fun addAddress(address: ValidatedAddress) {
- state.update { it.copy(addresses = (it.addresses + address).toImmutableList()) }
- }
-
- 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,
- ),
- addresses = persistentListOf(),
- onNameChange = ::onNameChange,
- onCloseClick = params.onBackClick,
- onAddAddressClick = ::requestAddAddress,
- )
+ stateController.update(AddValidatedAddressTransformer(address = address))
}
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/EditContactStateController.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/EditContactStateController.kt
new file mode 100644
index 0000000000..566148a41d
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/EditContactStateController.kt
@@ -0,0 +1,52 @@
+package com.tangem.features.addressbook.editcontact.state
+
+import com.tangem.common.ui.account.AccountIconUM
+import com.tangem.core.decompose.di.ModelScoped
+import com.tangem.core.ui.R
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.utils.transformer.Transformer
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@ModelScoped
+internal class EditContactStateController @Inject constructor() {
+
+ private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState())
+
+ val uiState: StateFlow get() = mutableUiState.asStateFlow()
+
+ fun update(transformer: Transformer) {
+ mutableUiState.update(function = transformer::transform)
+ }
+
+ private fun getInitialState(): EditContactUM {
+ val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
+ val selectedColor = colors.first()
+ return EditContactUM(
+ title = TextReference.EMPTY,
+ 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 = {},
+ ),
+ addresses = persistentListOf(),
+ onNameChange = {},
+ onCloseClick = {},
+ onAddAddressClick = {},
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/AddValidatedAddressTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/AddValidatedAddressTransformer.kt
new file mode 100644
index 0000000000..232b07210f
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/AddValidatedAddressTransformer.kt
@@ -0,0 +1,19 @@
+package com.tangem.features.addressbook.editcontact.state.transformers
+
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
+import com.tangem.utils.transformer.Transformer
+import kotlinx.collections.immutable.toImmutableList
+
+internal class AddValidatedAddressTransformer(
+ private val address: ValidatedAddress,
+) : Transformer {
+
+ override fun transform(prevState: EditContactUM): EditContactUM {
+ // Skip duplicates: an address is identified by its string value (it already carries all its networks).
+ if (prevState.addresses.any { it.address == address.address }) return prevState
+ return prevState.copy(
+ addresses = (prevState.addresses + address).toImmutableList(),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/SelectContactColorTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/SelectContactColorTransformer.kt
new file mode 100644
index 0000000000..fa2e4a0572
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/SelectContactColorTransformer.kt
@@ -0,0 +1,17 @@
+package com.tangem.features.addressbook.editcontact.state.transformers
+
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.utils.transformer.Transformer
+
+internal class SelectContactColorTransformer(
+ private val color: CryptoPortfolioIcon.Color,
+) : Transformer {
+
+ override fun transform(prevState: EditContactUM): EditContactUM {
+ return prevState.copy(
+ colors = prevState.colors.copy(selected = color),
+ portfolioIcon = prevState.portfolioIcon.copy(color = color),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateContactNameTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateContactNameTransformer.kt
new file mode 100644
index 0000000000..8b61afce92
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateContactNameTransformer.kt
@@ -0,0 +1,13 @@
+package com.tangem.features.addressbook.editcontact.state.transformers
+
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.utils.transformer.Transformer
+
+internal class UpdateContactNameTransformer(
+ private val name: String,
+) : Transformer {
+
+ override fun transform(prevState: EditContactUM): EditContactUM {
+ return prevState.copy(name = name)
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateEditContactInitialStateTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateEditContactInitialStateTransformer.kt
new file mode 100644
index 0000000000..26d8bdd3c8
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/state/transformers/UpdateEditContactInitialStateTransformer.kt
@@ -0,0 +1,35 @@
+package com.tangem.features.addressbook.editcontact.state.transformers
+
+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.ui.state.EditContactUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Wires the title (derived from whether an existing contact is being edited) and the callbacks owned by
+ * [com.tangem.features.addressbook.editcontact.model.EditContactModel] into the initial state.
+ */
+internal class UpdateEditContactInitialStateTransformer(
+ private val isExistingContact: Boolean,
+ private val onNameChange: (String) -> Unit,
+ private val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit,
+ private val onCloseClick: () -> Unit,
+ private val onAddAddressClick: () -> Unit,
+) : Transformer {
+
+ override fun transform(prevState: EditContactUM): EditContactUM {
+ val titleResId = if (isExistingContact) {
+ R.string.address_book_contact
+ } else {
+ R.string.address_book_new_contact
+ }
+ return prevState.copy(
+ title = resourceReference(titleResId),
+ colors = prevState.colors.copy(onColorSelect = onColorSelect),
+ onNameChange = onNameChange,
+ onCloseClick = onCloseClick,
+ onAddAddressClick = onAddAddressClick,
+ )
+ }
+}
\ 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
index e6d2f4a0ab..c45223210c 100644
--- 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
@@ -1,20 +1,16 @@
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.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Icon
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.graphics.vector.ImageVector
-import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
@@ -22,19 +18,27 @@ 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.SpacerH
import com.tangem.core.ui.components.account.AccountIconSize
+import com.tangem.core.ui.components.block.BlockCard
+import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.fields.AutoSizeTextField
+import com.tangem.core.ui.ds.image.TangemIcon
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.ds2.row.TangemRow
+import com.tangem.core.ui.ds2.row.TangemRowText
+import com.tangem.core.ui.ds2.row.TangemRowTextRole
+import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import com.tangem.core.ui.res.generated.icons.Icons
+import com.tangem.core.ui.res.generated.icons.ic_sign_plus_20
import com.tangem.domain.models.account.CryptoPortfolioIcon
-import com.tangem.features.addressbook.editcontact.contract.EditContactUM
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -63,118 +67,126 @@ internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modif
Column(
modifier = Modifier
- .padding(horizontal = 16.dp)
- .weight(1f),
- verticalArrangement = Arrangement.spacedBy(12.dp),
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
ContactSummary(state = state)
ContactColor(colors = state.colors)
- ContactAddresses(addresses = state.addresses)
- AddAddressRow(onClick = state.onAddAddressClick)
- }
- }
-}
-
-@Composable
-private fun ContactAddresses(addresses: ImmutableList) {
- if (addresses.isEmpty()) return
- Column(
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .fillMaxWidth()
- .background(TangemTheme.colors3.bg.secondary),
- ) {
- addresses.fastForEach { entry ->
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp, vertical = 12.dp),
- verticalArrangement = Arrangement.spacedBy(2.dp),
+ BlockCard(
+ shape = RoundedCornerShape(24.dp),
+ colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors3.bg.secondary),
) {
- Text(
- text = entry.network.name,
- style = TangemTheme.typography3.caption.medium,
- color = TangemTheme.colors3.text.tertiary,
- )
- Text(
- text = entry.address,
- style = TangemTheme.typography3.body.medium,
- color = TangemTheme.colors3.text.primary,
- maxLines = 1,
- )
+ ContactAddresses(addresses = state.addresses)
+ AddAddressRow(onClick = state.onAddAddressClick)
}
}
}
}
@Composable
-private fun AddAddressRow(onClick: () -> Unit) {
- Row(
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .fillMaxWidth()
- .background(TangemTheme.colors3.bg.secondary)
- .clickable(onClick = onClick)
- .padding(horizontal = 12.dp, vertical = 15.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .size(36.dp)
- .clip(CircleShape)
- .background(TangemTheme.colors3.bg.status.infoSubtle),
- ) {
- Icon(
- modifier = Modifier.size(18.dp),
- imageVector = ImageVector.vectorResource(R.drawable.ic_plus_24),
- tint = TangemTheme.colors3.text.status.info,
- contentDescription = null,
- )
- }
- Column(
- verticalArrangement = Arrangement.spacedBy(2.dp),
- ) {
- Text(
- text = stringResourceSafe(R.string.address_book_add_address),
- style = TangemTheme.typography3.body.medium,
- color = TangemTheme.colors3.text.primary,
- )
- Text(
- text = stringResourceSafe(R.string.address_book_add_address_description),
- style = TangemTheme.typography3.caption.medium,
- color = TangemTheme.colors3.text.tertiary,
- )
- }
+private fun ContactAddresses(addresses: ImmutableList) {
+ addresses.fastForEach { entry ->
+ AddressRow(entry = entry)
}
}
+@Composable
+private fun AddressRow(entry: ValidatedAddress) {
+ TangemRow(
+ verticalAlignment = TangemRowVerticalAlignment.Center,
+ startSlot = {
+ TangemIcon(
+ tangemIconUM = TangemIconUM.Ident(text = entry.address),
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape),
+ )
+ },
+ titleSlot = {
+ TangemRowText(
+ text = stringReference(entry.address),
+ role = TangemRowTextRole.Title,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+ },
+ subtitleSlot = {
+ TangemRowText(
+ text = pluralReference(
+ id = R.plurals.common_networks_count,
+ count = entry.networkIds.size,
+ formatArgs = wrappedList(entry.networkIds.size),
+ ),
+ role = TangemRowTextRole.Subtitle,
+ )
+ },
+ )
+}
+
+@Composable
+private fun AddAddressRow(onClick: () -> Unit) {
+ TangemRow(
+ verticalAlignment = TangemRowVerticalAlignment.Center,
+ onClick = onClick,
+ startSlot = {
+ TangemIcon(
+ tangemIconUM = TangemIconUM.Icon(
+ imageVector = Icons.ic_sign_plus_20,
+ tintReference = { TangemTheme.colors3.icon.brand },
+ ),
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ color = TangemTheme.colors3.bg.status.infoSubtle,
+ shape = RoundedCornerShape(10.dp),
+ )
+ .padding(8.dp),
+ )
+ },
+ titleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.address_book_add_address),
+ role = TangemRowTextRole.Title,
+ )
+ },
+ subtitleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.address_book_add_address_description),
+ role = TangemRowTextRole.Subtitle,
+ )
+ },
+ )
+}
+
@Composable
private fun ContactSummary(state: EditContactUM) {
val avatarName = state.name.ifBlank { state.namePlaceholder.resolveReference() }
Column(
modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
+ .clip(RoundedCornerShape(24.dp))
.fillMaxWidth()
- .background(TangemTheme.colors3.bg.secondary),
+ .background(TangemTheme.colors3.bg.secondary)
+ .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- Spacer(modifier = Modifier.height(24.dp))
+ SpacerH(20.dp)
AccountIcon(
name = stringReference(avatarName),
icon = state.portfolioIcon,
- size = AccountIconSize.Large,
+ size = AccountIconSize.RedesignLarge,
)
- Spacer(modifier = Modifier.height(24.dp))
+
+ SpacerH(28.dp)
Text(
text = stringResourceSafe(R.string.address_book_contact_name),
style = TangemTheme.typography3.caption.medium,
- color = TangemTheme.colors3.text.tertiary,
+ color = TangemTheme.colors3.text.secondary,
)
- Spacer(modifier = Modifier.height(2.dp))
+
+ SpacerH(4.dp)
AutoSizeTextField(
value = state.name,
@@ -186,7 +198,7 @@ private fun ContactSummary(state: EditContactUM) {
color = TangemTheme.colors3.text.primary,
placeholderColor = TangemTheme.colors3.text.tertiary,
)
- Spacer(modifier = Modifier.height(20.dp))
+ SpacerH(8.dp)
}
}
@@ -196,16 +208,16 @@ private fun ContactSummary(state: EditContactUM) {
private fun ContactColor(colors: EditContactUM.Colors) {
Box(
modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
+ .clip(RoundedCornerShape(24.dp))
.fillMaxWidth()
- .background(TangemTheme.colors3.bg.secondary),
+ .background(TangemTheme.colors3.bg.secondary)
+ .padding(16.dp),
) {
FlowRow(
maxItemsInEachRow = 6,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 12.dp),
- horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalArrangement = Arrangement.spacedBy(18.dp),
) {
colors.list.fastForEach { color ->
val isSelected = color == colors.selected
@@ -219,12 +231,12 @@ private fun ContactColor(colors: EditContactUM.Colors) {
if (isSelected) {
Box(
modifier = Modifier
- .size(47.dp)
+ .size(48.dp)
.border(2.dp, color.getUiColor(), shape = CircleShape),
)
Box(
modifier = Modifier
- .size(36.dp)
+ .size(38.dp)
.background(color = color.getUiColor(), shape = CircleShape),
)
} else {
@@ -260,7 +272,12 @@ private fun Preview_EditContactContent() {
list = colors,
onColorSelect = {},
),
- addresses = persistentListOf(),
+ addresses = persistentListOf(
+ ValidatedAddress(
+ address = "0x1234567890abcdef1234567890abcdef12345678",
+ networkIds = persistentListOf("ethereum", "bsc", "polygon"),
+ ),
+ ),
onNameChange = {},
onCloseClick = {},
onAddAddressClick = {},
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/ui/state/EditContactUM.kt
similarity index 87%
rename from features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt
rename to features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/state/EditContactUM.kt
index 9efd8db0e2..55793600ad 100644
--- 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/ui/state/EditContactUM.kt
@@ -1,10 +1,12 @@
-package com.tangem.features.addressbook.editcontact.contract
+package com.tangem.features.addressbook.editcontact.ui.state
+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,
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/state/ValidatedAddress.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/state/ValidatedAddress.kt
new file mode 100644
index 0000000000..8d36e4c6a8
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/state/ValidatedAddress.kt
@@ -0,0 +1,17 @@
+package com.tangem.features.addressbook.editcontact.ui.state
+
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * A recipient address validated on the AddAddress screen, together with the networks it resolves to.
+ *
+ * A single address can belong to several networks (e.g. the same address across EVM chains), so it carries a list of
+ * [networkIds]. This is the in-progress (pre-save) representation accumulated in [EditContactUM]; the [networkIds] are
+ * used to rebuild the domain `AddressEntry`s when the contact is persisted.
+ */
+@Immutable
+data class ValidatedAddress(
+ val address: String,
+ val networkIds: ImmutableList,
+)
\ 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
deleted file mode 100644
index 0072a79d9f..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/AddressBookListComponent.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-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 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 63733b90f9..6214fdbc75 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
@@ -1,22 +1,22 @@
package com.tangem.features.addressbook.list
+import androidx.compose.foundation.background
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.AddressBookListUM
+import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.core.ui.res.TangemTheme
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
+import com.tangem.features.addressbook.list.ui.state.AddressBookListUM
-internal class DefaultAddressBookListComponent @AssistedInject constructor(
- @Assisted context: AppComponentContext,
- @Assisted val params: AddressBookListComponent.Params,
-) : AddressBookListComponent, AppComponentContext by context {
+internal class DefaultAddressBookListComponent(
+ appComponentContext: AppComponentContext,
+ params: Params,
+) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: AddressBookListModel = getOrCreateModel(params)
@@ -25,19 +25,16 @@ internal class DefaultAddressBookListComponent @AssistedInject constructor(
val state by model.state.collectAsStateWithLifecycle()
when (val addressBookListUM = state) {
is AddressBookListUM.Empty -> AddressBookEmptyScreen(
- tangemButtonUM = addressBookListUM.tangemButtonUM,
+ onAddContactClick = addressBookListUM.onAddClick,
onBackClick = router::pop,
- modifier = modifier,
+ modifier = modifier.background(TangemTheme.colors3.bg.primary),
)
is AddressBookListUM.AddressList -> TODO("[REDACTED_TASK_KEY]")
}
}
- @AssistedFactory
- interface Factory : AddressBookListComponent.Factory {
- override fun create(
- context: AppComponentContext,
- params: AddressBookListComponent.Params,
- ): DefaultAddressBookListComponent
- }
+ 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/contract/AddressBookListUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt
deleted file mode 100644
index 4c0c74bab4..0000000000
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-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 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 27039d82aa..6d269a7616 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
@@ -3,18 +3,11 @@ 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.features.addressbook.list.DefaultAddressBookListComponent
+import com.tangem.features.addressbook.list.state.AddressBookListStateController
+import com.tangem.features.addressbook.list.state.transformers.UpdateAddressBookListInitialStateTransformer
+import com.tangem.features.addressbook.list.ui.state.AddressBookListUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@@ -22,22 +15,16 @@ import javax.inject.Inject
internal class AddressBookListModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
+ private val stateController: AddressBookListStateController,
) : Model() {
- private val params = paramsContainer.require()
+ private val params = paramsContainer.require()
- val state: StateFlow = MutableStateFlow(
- 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,
- ),
- ),
- )
+ val state: StateFlow get() = stateController.uiState
+
+ init {
+ stateController.update(
+ UpdateAddressBookListInitialStateTransformer(onAddContactClick = params.onAddContactClick),
+ )
+ }
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/AddressBookListStateController.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/AddressBookListStateController.kt
new file mode 100644
index 0000000000..f42ef0cd71
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/AddressBookListStateController.kt
@@ -0,0 +1,25 @@
+package com.tangem.features.addressbook.list.state
+
+import com.tangem.core.decompose.di.ModelScoped
+import com.tangem.features.addressbook.list.ui.state.AddressBookListUM
+import com.tangem.utils.transformer.Transformer
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@ModelScoped
+internal class AddressBookListStateController @Inject constructor() {
+
+ private val mutableUiState: MutableStateFlow =
+ MutableStateFlow(value = getInitialState())
+
+ val uiState: StateFlow get() = mutableUiState.asStateFlow()
+
+ fun update(transformer: Transformer) {
+ mutableUiState.update(function = transformer::transform)
+ }
+
+ private fun getInitialState(): AddressBookListUM = AddressBookListUM.Empty(onAddClick = {})
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/converter/ContactUMConverter.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/converter/ContactUMConverter.kt
new file mode 100644
index 0000000000..e50783cb34
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/converter/ContactUMConverter.kt
@@ -0,0 +1,21 @@
+package com.tangem.features.addressbook.list.state.converter
+
+import com.tangem.domain.addressbook.model.Contact
+import com.tangem.features.addressbook.list.ui.state.ContactUM
+import com.tangem.utils.converter.Converter
+
+/**
+ * Maps a domain [Contact] to its UI representation [ContactUM].
+ *
+ * TODO AddressBook ([REDACTED_TASK_KEY]): wire into [com.tangem.features.addressbook.list.model.AddressBookListModel] when the contacts list
+ * is loaded from the repository and the [com.tangem.features.addressbook.list.ui.state.AddressBookListUM.AddressList]
+ * screen is implemented.
+ */
+internal class ContactUMConverter : Converter {
+
+ override fun convert(value: Contact): ContactUM = ContactUM(
+ id = value.id.value,
+ name = value.name.value,
+ addressCount = value.addressEntries.size,
+ )
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListInitialStateTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListInitialStateTransformer.kt
new file mode 100644
index 0000000000..8191d0f1b5
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListInitialStateTransformer.kt
@@ -0,0 +1,19 @@
+package com.tangem.features.addressbook.list.state.transformers
+
+import com.tangem.features.addressbook.list.ui.state.AddressBookListUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Wires the "add contact" callback owned by the container into the initial (empty) list state.
+ */
+internal class UpdateAddressBookListInitialStateTransformer(
+ private val onAddContactClick: () -> Unit,
+) : Transformer {
+
+ override fun transform(prevState: AddressBookListUM): AddressBookListUM {
+ return when (prevState) {
+ is AddressBookListUM.Empty -> prevState.copy(onAddClick = onAddContactClick)
+ is AddressBookListUM.AddressList -> prevState
+ }
+ }
+}
\ 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 b1045b5cc2..50a6d9a30d 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
@@ -1,6 +1,5 @@
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.*
@@ -9,26 +8,26 @@ 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.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
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.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
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import com.tangem.core.ui.res.generated.icons.Icons
+import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
+import com.tangem.core.ui.res.generated.icons.ic_sign_plus_20
@Composable
internal fun AddressBookEmptyScreen(
- tangemButtonUM: TangemButtonUM,
+ onAddContactClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -41,26 +40,19 @@ internal fun AddressBookEmptyScreen(
title = resourceReference(R.string.address_book_title),
startContent = {
TangemButton(
- iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24),
+ iconStart = TangemIconUM.Icon(imageVector = Icons.ic_chevron_left_20),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
- NoContactInfo()
- PrimaryTangemButton(
- modifier = Modifier
- .fillMaxWidth()
- .navigationBarsPadding()
- .padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
- buttonUM = tangemButtonUM,
- )
+ NoContactInfo(onAddClick = onAddContactClick)
}
}
@Composable
-private fun ColumnScope.NoContactInfo() {
+private fun ColumnScope.NoContactInfo(onAddClick: () -> Unit) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
@@ -68,18 +60,24 @@ private fun ColumnScope.NoContactInfo() {
) {
ContactImage()
Text(
- modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
+ modifier = Modifier.padding(top = 32.dp),
text = stringResourceSafe(R.string.address_book_no_contacts),
color = TangemTheme.colors3.text.primary,
- style = TangemTheme.typography3.heading.medium,
+ style = TangemTheme.typography3.heading.small,
)
Text(
- modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
+ modifier = Modifier.padding(top = 8.dp),
text = stringResourceSafe(R.string.address_book_no_contacts_description),
color = TangemTheme.colors3.text.secondary,
- style = TangemTheme.typography3.body.medium,
+ style = TangemTheme.typography3.subheading.medium,
textAlign = TextAlign.Center,
)
+ TangemButton(
+ modifier = Modifier.padding(top = 40.dp),
+ text = resourceReference(R.string.address_book_add_address),
+ onClick = onAddClick,
+ iconEnd = TangemIconUM.Icon(imageVector = Icons.ic_sign_plus_20),
+ )
}
}
@@ -95,7 +93,7 @@ private fun ContactImage() {
contentAlignment = Alignment.Center,
) {
Image(
- painter = painterResource(R.drawable.ic_contact_20),
+ imageVector = ImageVector.vectorResource(R.drawable.ic_address_book_24),
contentDescription = stringResourceSafe(R.string.address_book_no_contacts),
modifier = Modifier.size(28.dp),
)
@@ -104,16 +102,11 @@ private fun ContactImage() {
@Composable
@Preview(showBackground = true, widthDp = 360)
-@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_AddressBookEmptyScreen() {
- 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 = {},
- )
+ TangemThemePreviewRedesign {
+ AddressBookEmptyScreen(
+ onAddContactClick = {},
+ onBackClick = {},
+ )
+ }
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/state/AddressBookListUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/state/AddressBookListUM.kt
new file mode 100644
index 0000000000..e1b9061014
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/state/AddressBookListUM.kt
@@ -0,0 +1,12 @@
+package com.tangem.features.addressbook.list.ui.state
+
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+internal sealed interface AddressBookListUM {
+
+ data class Empty(val onAddClick: () -> Unit) : 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/state/ContactUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/state/ContactUM.kt
new file mode 100644
index 0000000000..c5f526a6bc
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/state/ContactUM.kt
@@ -0,0 +1,11 @@
+package com.tangem.features.addressbook.list.ui.state
+
+import androidx.compose.runtime.Immutable
+
+/** UI model of a single address-book contact row. Holds only what the list needs to render — no domain types. */
+@Immutable
+internal data class ContactUM(
+ val id: String,
+ val name: String,
+ val addressCount: Int,
+)
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/route/AddressBookRoute.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/route/AddressBookRoute.kt
new file mode 100644
index 0000000000..c59c929b70
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/route/AddressBookRoute.kt
@@ -0,0 +1,27 @@
+package com.tangem.features.addressbook.route
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal sealed class AddressBookRoute {
+
+ @Serializable
+ data object List : AddressBookRoute()
+
+ /**
+ * if [contactId] is not null we should fetch existing contact.
+ *
+ * [predefinedAddress] and [predefinedNetworkId] are set only when the feature is opened in
+ * [com.tangem.features.addressbook.entity.AddressBookOpenMode.WithContactCreation] mode — the address and its
+ * network are already known, so the new contact is opened with that address already attached.
+ */
+ @Serializable
+ data class EditContact(
+ val contactId: String? = null,
+ val predefinedAddress: String? = null,
+ val predefinedNetworkId: String? = null,
+ ) : AddressBookRoute()
+
+ @Serializable
+ data object AddAddress : AddressBookRoute()
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModelTest.kt
index c58bb25f8d..cce48d3bbe 100644
--- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModelTest.kt
+++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/addaddress/model/AddAddressModelTest.kt
@@ -3,24 +3,23 @@ package com.tangem.features.addressbook.addaddress.model
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
-import com.tangem.common.ui.extensions.iconResId
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.clipboard.ClipboardManager
+import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
-import com.tangem.domain.models.network.Network
-import com.tangem.features.addressbook.addaddress.AddAddressComponent
-import com.tangem.features.addressbook.addaddress.contract.AddAddressUM
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
+import com.tangem.features.addressbook.addaddress.DefaultAddAddressComponent
+import com.tangem.features.addressbook.addaddress.state.AddAddressStateController
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
import com.tangem.test.mock.MockAccounts
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
-import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
@@ -28,11 +27,7 @@ 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.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Nested
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.*
@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -73,7 +68,6 @@ internal class AddAddressModelTest {
// Assert
assertThat(state.addressField.value).isEmpty()
- assertThat(state.addressField.isValuePasted).isFalse()
assertThat(state.buttonUM.isEnabled).isFalse()
}
@@ -87,13 +81,11 @@ internal class AddAddressModelTest {
model.state.value.onAddressChange(address)
// Assert
- val field = model.state.value.addressField
- assertThat(field.value).isEqualTo(address)
- assertThat(field.isValuePasted).isFalse()
+ assertThat(model.state.value.addressField.value).isEqualTo(address)
}
@Test
- fun `GIVEN empty field WHEN onPasteClick THEN value marked as pasted`() = runTest {
+ fun `GIVEN empty field WHEN onPasteClick THEN value taken from clipboard`() = runTest {
// Arrange
val model = createModel(testScope = this)
val address = "0xABC"
@@ -103,13 +95,10 @@ internal class AddAddressModelTest {
model.state.value.onPasteClick()
// Assert
- val field = model.state.value.addressField
- assertThat(field.value).isEqualTo(address)
- assertThat(field.isValuePasted).isTrue()
+ assertThat(model.state.value.addressField.value).isEqualTo(address)
}
// validateAndConfirm() is an unimplemented seam — the button click must NOT emit a result yet.
- // This guards the foundation and will fail (prompting an update) once validation is wired in.
@Test
fun `GIVEN typed address WHEN button clicked THEN onConfirm not called yet`() = runTest {
// Arrange
@@ -127,13 +116,12 @@ internal class AddAddressModelTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
- inner class AddressInput {
+ inner class Validation {
@Test
- fun `GIVEN coins available WHEN valid address typed THEN matching network chosen`() = runTest {
+ fun `GIVEN coins available WHEN valid address typed THEN no error AND button enabled`() = runTest {
// Arrange
- every { multiAccountListSupplier.invoke() } returns
- flowOf(listOf(accountListWith(ethereum, bitcoin)))
+ every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountListWith(ethereum, bitcoin)))
val model = createModel(testScope = this)
advanceUntilIdle()
@@ -143,16 +131,14 @@ internal class AddAddressModelTest {
// Assert
val state = model.state.value
- assertThat(state.availableNetworks).containsExactly(ethereum.network)
- assertThat(state.chosenNetworkStateUM)
- .isEqualTo(resultOf(ethereum.network))
+ assertThat(state.addressField.isError).isFalse()
+ assertThat(state.buttonUM.isEnabled).isTrue()
}
@Test
- fun `GIVEN coins available WHEN address matches no network THEN empty state`() = runTest {
+ fun `GIVEN coins available WHEN address matches no network THEN error AND button disabled`() = runTest {
// Arrange
- every { multiAccountListSupplier.invoke() } returns
- flowOf(listOf(accountListWith(ethereum, bitcoin)))
+ every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountListWith(ethereum, bitcoin)))
val model = createModel(testScope = this)
advanceUntilIdle()
@@ -162,31 +148,32 @@ internal class AddAddressModelTest {
// Assert
val state = model.state.value
- assertThat(state.availableNetworks).isEmpty()
- assertThat(state.chosenNetworkStateUM).isEqualTo(AddAddressUM.ChosenNetworkStateUM.Empty)
+ assertThat(state.addressField.isError).isTrue()
+ assertThat(state.addressField.label)
+ .isEqualTo(resourceReference(R.string.address_book_invalid_address_error))
+ assertThat(state.buttonUM.isEnabled).isFalse()
}
@Test
- fun `GIVEN no coins available WHEN valid address typed THEN empty state`() = runTest {
- // Arrange — supplier emits no accounts.
- every { multiAccountListSupplier.invoke() } returns flowOf(emptyList())
+ fun `GIVEN empty address WHEN validated THEN no error AND button disabled`() = runTest {
+ // Arrange
+ every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountListWith(ethereum)))
val model = createModel(testScope = this)
advanceUntilIdle()
// Act
- model.state.value.onAddressChange(VALID_ETH_ADDRESS)
+ model.state.value.onAddressChange("")
advanceUntilIdle()
// Assert
val state = model.state.value
- assertThat(state.availableNetworks).isEmpty()
- assertThat(state.chosenNetworkStateUM).isEqualTo(AddAddressUM.ChosenNetworkStateUM.Empty)
+ assertThat(state.addressField.isError).isFalse()
+ assertThat(state.buttonUM.isEnabled).isFalse()
}
- // Covers the "not initialized yet" case: the address is typed before coins load, and the
- // chosen network must resolve reactively once the supplier emits them.
+ // The address is typed before coins load; validity must resolve reactively once the supplier emits them.
@Test
- fun `GIVEN address typed before coins load WHEN coins emitted THEN network resolved reactively`() = runTest {
+ fun `GIVEN address typed before coins load WHEN coins emitted THEN validated reactively`() = runTest {
// Arrange
val accountsFlow = MutableStateFlow>(emptyList())
every { multiAccountListSupplier.invoke() } returns accountsFlow
@@ -197,29 +184,19 @@ internal class AddAddressModelTest {
model.state.value.onAddressChange(VALID_ETH_ADDRESS)
advanceUntilIdle()
// Assert intermediate: nothing to match yet
- assertThat(model.state.value.chosenNetworkStateUM).isEqualTo(AddAddressUM.ChosenNetworkStateUM.Empty)
+ assertThat(model.state.value.buttonUM.isEnabled).isFalse()
// Act — coins arrive later
accountsFlow.value = listOf(accountListWith(ethereum, bitcoin))
advanceUntilIdle()
// Assert
- assertThat(model.state.value.chosenNetworkStateUM)
- .isEqualTo(resultOf(ethereum.network))
+ val state = model.state.value
+ assertThat(state.buttonUM.isEnabled).isTrue()
+ assertThat(state.addressField.isError).isFalse()
}
}
- private fun resultOf(vararg networks: Network) = AddAddressUM.ChosenNetworkStateUM.Result(
- networkUMList = networks
- .map { network ->
- AddAddressUM.ChosenNetworkStateUM.Result.NetworkUM(
- networkName = network.name,
- iconResId = network.iconResId,
- )
- }
- .toImmutableList(),
- )
-
private fun accountListWith(vararg currencies: CryptoCurrency): AccountList {
val walletId = MockAccounts.userWalletId
val accounts = listOf(
@@ -239,7 +216,7 @@ internal class AddAddressModelTest {
private fun createModel(
testScope: TestScope,
onConfirm: (ValidatedAddress) -> Unit = {},
- params: AddAddressComponent.Params = AddAddressComponent.Params(
+ params: DefaultAddAddressComponent.Params = DefaultAddAddressComponent.Params(
onBackClick = {},
onConfirm = onConfirm,
),
@@ -250,6 +227,7 @@ internal class AddAddressModelTest {
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
multiAccountListSupplier = multiAccountListSupplier,
clipboardManager = clipboardManager,
+ stateController = AddAddressStateController(),
).also { model = it }
}
diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt
index ebf9acafd1..cdf098ec82 100644
--- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt
+++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt
@@ -8,23 +8,36 @@ 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.domain.models.network.Network
-import com.tangem.features.addressbook.editcontact.EditContactComponent
-import com.tangem.features.addressbook.editcontact.contract.EditContactUM
-import com.tangem.features.addressbook.editcontact.contract.ValidatedAddress
+import com.tangem.features.addressbook.common.AddressBookResultHolder
+import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
+import com.tangem.features.addressbook.editcontact.state.EditContactStateController
+import com.tangem.features.addressbook.editcontact.ui.state.EditContactUM
+import com.tangem.features.addressbook.editcontact.ui.state.ValidatedAddress
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
-import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
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.AfterEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class EditContactModelTest {
+ private val resultHolder = AddressBookResultHolder()
+
+ private var model: EditContactModel? = null
+
+ @AfterEach
+ fun tearDown() {
+ // Cancels modelScope, stopping the confirmed-addresses collector.
+ model?.onDestroy()
+ model = null
+ }
+
@Test
fun `WHEN model created THEN initial state is correct`() = runTest {
val expectedColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
@@ -57,11 +70,7 @@ internal class EditContactModelTest {
@Test
fun `GIVEN existing contactId WHEN model created THEN title is contact`() = runTest {
// Arrange
- val params = EditContactComponent.Params(
- contactId = ContactId(value = "contact-id"),
- onBackClick = {},
- onAddAddressClick = {},
- )
+ val params = createParams(contactId = ContactId(value = "contact-id"))
// Act
val model = createModel(testScope = this, params = params)
@@ -94,38 +103,73 @@ internal class EditContactModelTest {
}
@Test
- fun `GIVEN add address requested WHEN result delivered THEN address appended to state`() = runTest {
+ fun `GIVEN confirmed address set on holder WHEN collected THEN address appended to state`() = runTest {
// Arrange
- var capturedSink: ((ValidatedAddress) -> Unit)? = null
- val params = EditContactComponent.Params(
- contactId = null,
- onBackClick = {},
- onAddAddressClick = { onResult -> capturedSink = onResult },
- )
- val model = createModel(testScope = this, params = params)
- val validatedAddress = ValidatedAddress(address = "0xABC", network = mockk())
+ val model = createModel(testScope = this)
+ advanceUntilIdle()
+ val validatedAddress = ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))
// Act
- model.state.value.onAddAddressClick()
- capturedSink?.invoke(validatedAddress)
+ resultHolder.setConfirmedAddress(validatedAddress)
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(model.state.value.addresses).containsExactly(validatedAddress)
+ // The value must be consumed so it is not re-applied on resubscription.
+ assertThat(resultHolder.confirmedAddress.value).isNull()
+ }
+
+ @Test
+ fun `GIVEN same address confirmed twice WHEN collected THEN added only once`() = runTest {
+ // Arrange
+ val model = createModel(testScope = this)
+ advanceUntilIdle()
+ val validatedAddress = ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))
+
+ // Act
+ resultHolder.setConfirmedAddress(validatedAddress)
+ advanceUntilIdle()
+ resultHolder.setConfirmedAddress(validatedAddress)
+ advanceUntilIdle()
// Assert
assertThat(model.state.value.addresses).containsExactly(validatedAddress)
}
+ @Test
+ fun `GIVEN predefined address WHEN model created THEN address attached`() = runTest {
+ // Arrange
+ val predefined = ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))
+
+ // Act
+ val model = createModel(testScope = this, params = createParams(predefinedAddress = predefined))
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(model.state.value.addresses).containsExactly(predefined)
+ }
+
+ private fun createParams(
+ contactId: ContactId? = null,
+ predefinedAddress: ValidatedAddress? = null,
+ ): DefaultEditContactComponent.Params = DefaultEditContactComponent.Params(
+ contactId = contactId,
+ predefinedAddress = predefinedAddress,
+ onBackClick = {},
+ onAddAddressClick = {},
+ )
+
private fun createModel(
testScope: TestScope,
- params: EditContactComponent.Params = EditContactComponent.Params(
- contactId = null,
- onBackClick = {},
- onAddAddressClick = {},
- ),
+ params: DefaultEditContactComponent.Params = createParams(),
paramsContainer: ParamsContainer = MutableParamsContainer(value = params),
): EditContactModel {
return EditContactModel(
paramsContainer = paramsContainer,
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
- )
+ stateController = EditContactStateController(),
+ resultHolder = resultHolder,
+ ).also { model = it }
}
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt
index e7849eebed..7082a0c24e 100644
--- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt
+++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt
@@ -25,10 +25,11 @@ internal sealed class DetailsItemUM {
override val id: String = "wallet_connect"
}
- data class WalletConnectAddressBookBlock(val items: List- ) : DetailsItemUM() {
+ data class WalletActionBlock(val items: ImmutableList
- ) : DetailsItemUM() {
override val id: String = "wallet_connect_address_book"
sealed class Item(open val onClick: () -> Unit) {
+
data class WalletConnect(override val onClick: () -> Unit) : Item(onClick)
data class AddressBook(override val onClick: () -> Unit) : Item(onClick)
}
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 427c80adce..48d51c0835 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
@@ -2,13 +2,13 @@ package com.tangem.features.details.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
@@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
@@ -27,19 +28,27 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.BlockItem
-import com.tangem.core.ui.components.inputrow.InputRowImageBase
import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds2.row.TangemRow
+import com.tangem.core.ui.ds2.row.TangemRowText
+import com.tangem.core.ui.ds2.row.TangemRowTextRole
+import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.core.ui.res.generated.icons.Icons
+import com.tangem.core.ui.res.generated.icons.ic_chevron_right_20
import com.tangem.core.ui.test.DetailsScreenTestTags
import com.tangem.features.details.component.preview.PreviewDetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.entity.DetailsUM
import com.tangem.features.details.impl.R
+import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun DetailsScreen(
@@ -159,9 +168,9 @@ private fun Block(
onClick = model.onClick,
)
}
- is DetailsItemUM.WalletConnectAddressBookBlock -> {
+ is DetailsItemUM.WalletActionBlock -> {
BlockCard {
- WalletConnectAddressBookBlockItems(
+ WalletActionsBlock(
items = model.items,
modifier = itemModifier,
)
@@ -170,37 +179,106 @@ private fun Block(
is DetailsItemUM.UserWalletList -> {
userWalletListBlockContent.Content(modifier = itemModifier)
}
- is DetailsItemUM.UnderSectionText -> { /* Handled above */
- }
+ is DetailsItemUM.UnderSectionText -> Unit
}
}
}
@Composable
-private fun WalletConnectAddressBookBlockItems(
- items: List,
+private fun WalletActionsBlock(
+ items: ImmutableList,
modifier: Modifier = Modifier,
) {
items.fastForEach { item ->
when (item) {
- is DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect -> InputRowImageBase(
- 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.WalletActionBlock.Item.WalletConnect -> WalletConnectActionRow(
+ onClick = item.onClick,
+ modifier = modifier,
)
- is DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook -> InputRowImageBase(
- 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),
- caption = TextReference.Res(R.string.address_book_description),
+ is DetailsItemUM.WalletActionBlock.Item.AddressBook -> AddressBookActionRow(
+ onClick = item.onClick,
+ modifier = modifier,
)
}
}
}
+@Composable
+private fun WalletConnectActionRow(onClick: () -> Unit, modifier: Modifier = Modifier) {
+ TangemRow(
+ verticalAlignment = TangemRowVerticalAlignment.Center,
+ modifier = modifier,
+ onClick = onClick,
+ startSlot = {
+ TangemIcon(
+ tangemIconUM = TangemIconUM.Image(R.drawable.img_wallet_connect_76),
+ modifier = Modifier
+ .size(40.dp)
+ .clip(RoundedCornerShape(12.dp)),
+ )
+ },
+ titleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.wallet_connect_title),
+ role = TangemRowTextRole.Title,
+ )
+ },
+ subtitleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.wallet_connect_subtitle),
+ role = TangemRowTextRole.Subtitle,
+ )
+ },
+ endSlot = { ActionRowChevron() },
+ )
+}
+
+@Composable
+private fun AddressBookActionRow(onClick: () -> Unit, modifier: Modifier = Modifier) {
+ TangemRow(
+ verticalAlignment = TangemRowVerticalAlignment.Center,
+ modifier = modifier,
+ onClick = onClick,
+ startSlot = {
+ TangemIcon(
+ tangemIconUM = TangemIconUM.Icon(
+ iconRes = R.drawable.ic_address_book_24,
+ tintReference = { TangemTheme.colors3.icon.brand },
+ ),
+ modifier = Modifier
+ .size(40.dp)
+ .background(
+ color = TangemTheme.colors3.bg.status.infoSubtle,
+ shape = RoundedCornerShape(12.dp),
+ )
+ .padding(8.dp),
+ )
+ },
+ titleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.address_book_title),
+ role = TangemRowTextRole.Title,
+ )
+ },
+ subtitleSlot = {
+ TangemRowText(
+ text = TextReference.Res(R.string.address_book_description),
+ role = TangemRowTextRole.Subtitle,
+ )
+ },
+ endSlot = { ActionRowChevron() },
+ )
+}
+
+@Composable
+private fun ActionRowChevron() {
+ Icon(
+ imageVector = Icons.ic_chevron_right_20,
+ contentDescription = null,
+ tint = TangemTheme.colors3.icon.secondary,
+ )
+}
+
@Composable
private fun UnderSectionTextBlock(text: TextReference, modifier: Modifier = Modifier) {
Text(
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 df2e653821..99d34d298d 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
@@ -35,7 +35,7 @@ internal class ItemsBuilder @Inject constructor(
onBuyClick: () -> Unit,
): ImmutableList = buildList {
if (isAddressBookAvailable) {
- buildWalletConnectAddressBookBlock(isWalletConnectAvailable, userWalletId)
+ buildWalletActionBlock(isWalletConnectAvailable, userWalletId)
} else {
buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add)
}
@@ -91,29 +91,29 @@ internal class ItemsBuilder @Inject constructor(
}
}
- private fun MutableList.buildWalletConnectAddressBookBlock(
+ private fun MutableList.buildWalletActionBlock(
isWalletConnectAvailable: Boolean,
userWalletId: UserWalletId,
) {
- val walletConnectAddressBookItems = buildList {
+ val walletActionItems = buildList {
if (isWalletConnectAvailable) add(buildWalletConnectButton(userWalletId))
add(buildAddressBookButton())
- }
- if (walletConnectAddressBookItems.isNotEmpty()) {
- add(DetailsItemUM.WalletConnectAddressBookBlock(walletConnectAddressBookItems))
+ }.toImmutableList()
+ if (walletActionItems.isNotEmpty()) {
+ add(DetailsItemUM.WalletActionBlock(walletActionItems))
}
}
private fun buildWalletConnectButton(
userWalletId: UserWalletId,
- ): DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect {
- return DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect(
+ ): DetailsItemUM.WalletActionBlock.Item.WalletConnect {
+ return DetailsItemUM.WalletActionBlock.Item.WalletConnect(
onClick = { router.push(AppRoute.WalletConnectSessions(userWalletId)) },
)
}
- private fun buildAddressBookButton(): DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook {
- return DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook(
+ private fun buildAddressBookButton(): DetailsItemUM.WalletActionBlock.Item.AddressBook {
+ return DetailsItemUM.WalletActionBlock.Item.AddressBook(
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 d6962201eb..6f4c22d644 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
@@ -65,10 +65,10 @@ internal class ItemsBuilderTest {
"support",
).inOrder()
- val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock
+ val block = result.first() as DetailsItemUM.WalletActionBlock
assertThat(block.items.map { it::class.java }).containsExactly(
- DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect::class.java,
- DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook::class.java,
+ DetailsItemUM.WalletActionBlock.Item.WalletConnect::class.java,
+ DetailsItemUM.WalletActionBlock.Item.AddressBook::class.java,
).inOrder()
}
@@ -86,9 +86,9 @@ internal class ItemsBuilderTest {
"support",
).inOrder()
- val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock
+ val block = result.first() as DetailsItemUM.WalletActionBlock
assertThat(block.items.map { it::class.java }).containsExactly(
- DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook::class.java,
+ DetailsItemUM.WalletActionBlock.Item.AddressBook::class.java,
)
}
@@ -109,9 +109,9 @@ internal class ItemsBuilderTest {
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 block = result.first() as DetailsItemUM.WalletActionBlock
val walletConnect = block.items
- .filterIsInstance()
+ .filterIsInstance()
.single()
// Act
@@ -125,9 +125,9 @@ internal class ItemsBuilderTest {
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 block = result.first() as DetailsItemUM.WalletActionBlock
val addressBook = block.items
- .filterIsInstance()
+ .filterIsInstance()
.single()
// Act