Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-22 19:36:40 +02:00
parent de3cc4df6e
commit cf28f21151
3 changed files with 75 additions and 39 deletions

View file

@ -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<Pair<String, Set<String>>>,
)
private data class EditSnapshot(

View file

@ -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,

View file

@ -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