Updated on 2026-08-14
This commit is contained in:
parent
de3cc4df6e
commit
cf28f21151
3 changed files with 75 additions and 39 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue