From cf28f211519c02b32e95143d8744b1d0a9c531c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Jul 2026 19:36:40 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../editcontact/model/EditContactModel.kt | 15 +++-- .../editcontact/ui/EditContactContent.kt | 65 +++++++++---------- .../editcontact/model/EditContactModelTest.kt | 34 ++++++++++ 3 files changed, 75 insertions(+), 39 deletions(-) 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 ac64bcc2ba..6d4c27302b 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 @@ -243,18 +243,18 @@ internal class EditContactModel @Inject constructor( } private fun observeSaveButton() { - // Recompute on input changes and on wallet changes (the wallet type drives the button's Tangem-logo icon). combine( stateController.uiState .map { state -> SaveButtonInputs( name = state.name, hasNameError = state.nameError != null, - hasAddresses = state.addresses.isNotEmpty(), + colorName = state.colors.selected.name, + addresses = state.addresses.map { it.address to it.networkIds.toSet() }, ) } .distinctUntilChanged(), - selectedWallet.map { it is UserWallet.Cold }.distinctUntilChanged(), + selectedWallet.map { it?.walletId to (it is UserWallet.Cold) }.distinctUntilChanged(), ) { _, _ -> } .onEach { refreshSaveButton() } .launchIn(modelScope) @@ -449,7 +449,11 @@ internal class EditContactModel @Inject constructor( private fun refreshSaveButton() { val ui = stateController.uiState.value val isSaving = saveJob?.isActive == true - val isEnabled = ui.name.isNotBlank() && ui.nameError == null && ui.addresses.isNotEmpty() && !isSaving + val isEnabled = ui.name.isNotBlank() && + ui.nameError == null && + ui.addresses.isNotEmpty() && + isDirty() && + !isSaving stateController.update( UpdateSaveButtonTransformer( isEnabled = isEnabled, @@ -574,7 +578,8 @@ internal class EditContactModel @Inject constructor( private data class SaveButtonInputs( val name: String, val hasNameError: Boolean, - val hasAddresses: Boolean, + val colorName: String, + val addresses: List>>, ) private data class EditSnapshot( 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 0de0bc005c..8533712849 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 @@ -62,49 +62,46 @@ internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modif .background(color = TangemTheme.colors3.bg.primary), ) { var topBarHeightPx by remember { mutableIntStateOf(0) } + var saveButtonHeightPx by remember { mutableIntStateOf(0) } val topBarHeight = with(LocalDensity.current) { topBarHeightPx.toDp() } + val saveButtonHeight = with(LocalDensity.current) { saveButtonHeightPx.toDp() } - BoxWithConstraints( + Column( modifier = Modifier .fillMaxSize() .hazeSourceTangem() .background(color = TangemTheme.colors3.bg.primary) .imePadding() - .navigationBarsPadding(), + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + .padding(top = topBarHeight + 12.dp, bottom = saveButtonHeight + 12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - val minContentHeight = maxHeight - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), + ContactSummary(state = state) + ContactColor(colors = state.colors) + BlockCard( + shape = RoundedCornerShape(24.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors3.bg.secondary), ) { - Column( - modifier = Modifier - .heightIn(min = minContentHeight) - .padding(horizontal = 16.dp) - .padding(top = topBarHeight + 12.dp, bottom = 12.dp), - ) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - ContactSummary(state = state) - ContactColor(colors = state.colors) - BlockCard( - shape = RoundedCornerShape(24.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors3.bg.secondary), - ) { - ContactAddresses(addresses = state.addresses, onAddressClick = state.onAddressClick) - AddAddressRow(isEnabled = state.isAddAddressEnabled, onClick = state.onAddAddressClick) - } - WalletBlock(walletBlock = state.walletBlock) - state.onDeleteClick?.let { - DeleteContactButton(onClick = it) - } - } - Spacer(modifier = Modifier.weight(1f)) - SaveButton(saveButton = state.saveButton) - } + ContactAddresses(addresses = state.addresses, onAddressClick = state.onAddressClick) + AddAddressRow(isEnabled = state.isAddAddressEnabled, onClick = state.onAddAddressClick) + } + WalletBlock(walletBlock = state.walletBlock) + state.onDeleteClick?.let { + DeleteContactButton(onClick = it) } } + SaveButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .imePadding() + .navigationBarsPadding() + .onSizeChanged { saveButtonHeightPx = it.height }, + saveButton = state.saveButton, + ) + TangemTopNavigation( modifier = Modifier .align(Alignment.TopCenter) @@ -117,11 +114,11 @@ internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modif } @Composable -private fun SaveButton(saveButton: TangemButtonUM) { +private fun SaveButton(saveButton: TangemButtonUM, modifier: Modifier = Modifier) { TangemButton( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .padding(top = 16.dp), + .padding(horizontal = 16.dp, vertical = 12.dp), text = saveButton.text, iconEnd = saveButton.tangemIconUM, onClick = saveButton.onClick, 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 d82118a103..ce2eab33d8 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 @@ -638,6 +638,40 @@ internal class EditContactModelTest { assertThat(state.addresses.map { it.address }).containsExactly("0xABC") } + @Test + fun `GIVEN existing contact WHEN model created AND nothing changed THEN save button disabled`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + val contact = existingContact(walletId = "aa", name = "Alice", address = "0xABC") + every { getContactByIdUseCase(ContactId("c-1")) } returns MutableStateFlow(contact) + + // Act + val model = createModel(testScope = this, params = createParams(contactId = ContactId("c-1"))) + advanceUntilIdle() + + // Assert — a valid contact that was only opened (not edited) must not be saveable. + assertThat(model.state.value.saveButton.isEnabled).isFalse() + } + + @Test + fun `GIVEN existing contact WHEN name changed THEN save button enabled`() = runTest { + // Arrange + val walletA = createWallet(id = "aa", name = "Wallet A") + setupWallets(wallets = listOf(walletA), selected = walletA) + val contact = existingContact(walletId = "aa", name = "Alice", address = "0xABC") + every { getContactByIdUseCase(ContactId("c-1")) } returns MutableStateFlow(contact) + val model = createModel(testScope = this, params = createParams(contactId = ContactId("c-1"))) + advanceUntilIdle() + + // Act + model.state.value.onNameChange("Alice edited") + advanceUntilIdle() + + // Assert + assertThat(model.state.value.saveButton.isEnabled).isTrue() + } + @Test fun `GIVEN existing contact WHEN save clicked THEN updateContact called instead of create`() = runTest { // Arrange